0
0
Fork 0
mirror of https://github.com/discourse/discourse.git synced 2026-08-06 13:08:40 +08:00
discourse/plugins/discourse-calendar/test/javascripts/integration/components/post-event-builder-test.gjs
Martin Brennan 2ff5b99a4d
FEATURE: Embedded Zoom webinar for livestream events (#40973)
This commit adds support for embedding a Zoom webinar
livestream directly within a Discourse Event via a URL in
the location field of the event in the topic.

Admins must supply a Zoom API key and secret in the plugin
settings for this functionality to be available using
these settings:

* livestream_zoom_enabled
* livestream_zoom_sdk_key
* livestream_zoom_sdk_secret

When a topic is opened with a Zoom URL in the event location,
and the Livestream toggle is enabled, the
Zoom library dependencies will be loaded asyncrhonously
and the Zoom webinar will be embedded within the topic page
using the Zoom component view.

On mobile, a fullscreen Zoom client using the Zoom client view
will be used, with a floating Chat button that opens the Discourse
livestream chat in a modal which slides up from the bottom
of the screen in an overlay. This client view is at `/t/:id/:slug/zoom` 

### Webinar event date/time not reached

<img width="880" height="612" alt="image"
src="https://github.com/user-attachments/assets/4b112b59-eed0-4d63-b166-da825d8ebd0b"
/>

### Within 30 minutes before and 10 minutes after the event date/time

<img width="879" height="613" alt="image"
src="https://github.com/user-attachments/assets/373191f8-6245-4b41-b2a8-161c935166be"
/>

### Attendee joined before host, we will retry join every 30s

<img width="735" height="817" alt="image"
src="https://github.com/user-attachments/assets/519afa51-8099-4af0-954f-08da4c7e3297"
/>

### Attendee joined

<img width="733" height="831" alt="image"
src="https://github.com/user-attachments/assets/f633d21f-956e-4bec-a2f8-f7dc8b95d974"
/>

---------

Co-authored-by: David Taylor <david@taylorhq.com>
Co-authored-by: awesomerobot <kris.aubuchon@discourse.org>
2026-07-16 10:58:56 +10:00

156 lines
4.2 KiB
Text
Vendored

import { getOwner } from "@ember/owner";
import { fillIn, render } from "@ember/test-helpers";
import { module, test } from "qunit";
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
import PostEventBuilder from "../../discourse/components/modal/post-event-builder";
import DiscoursePostEventEvent from "../../discourse/models/discourse-post-event-event";
module("Integration | Component | Modal | PostEventBuilder", function (hooks) {
setupRenderingTest(hooks);
hooks.beforeEach(function () {
const store = getOwner(this).lookup("service:store");
this.user = store.createRecord("user", {
username: "tom",
id: 1,
admin: true,
});
getOwner(this).unregister("service:current-user");
getOwner(this).register("service:current-user", this.user, {
instantiate: false,
});
});
test("advanced screen renders an existing image upload as its URL", async function (assert) {
const event = DiscoursePostEventEvent.create({
name: "My event",
starts_at: "2022-07-01T10:00:00Z",
ends_at: "2022-07-01T11:00:00Z",
timezone: "UTC",
status: "public",
reminders: [],
raw_invitees: [],
custom_fields: {},
image_upload: {
url: "/uploads/default/original/1X/test-event-image.png",
short_url: "upload://test-event-image",
},
});
const model = {
event,
initialScreen: "advanced",
onUpdate: () => {},
toolbarEvent: {},
};
const closeModal = () => {};
await render(
<template>
<PostEventBuilder
@inline={{true}}
@model={{model}}
@closeModal={{closeModal}}
/>
</template>
);
assert
.dom(`[data-name="imageUpload"] .file-uploader`)
.hasClass("has-image", "the image control shows the uploaded image");
assert
.dom(`[data-name="imageUpload"] .file-uploader__preview`)
.hasAttribute(
"style",
/url\(\/uploads\/default\/original\/1X\/test-event-image\.png\)/,
"renders the upload url, not the raw object"
);
});
test("advanced screen renders a custom field whose name contains a dash", async function (assert) {
this.siteSettings.discourse_post_event_allowed_custom_fields = "field-aa";
const event = DiscoursePostEventEvent.create({
name: "My event",
starts_at: "2022-07-01T10:00:00Z",
ends_at: "2022-07-01T11:00:00Z",
timezone: "UTC",
status: "public",
reminders: [],
raw_invitees: [],
custom_fields: {},
});
const model = {
event,
initialScreen: "advanced",
onUpdate: () => {},
toolbarEvent: {},
};
const closeModal = () => {};
await render(
<template>
<PostEventBuilder
@inline={{true}}
@model={{model}}
@closeModal={{closeModal}}
/>
</template>
);
assert
.dom(`[data-name="customFields.field_aa"] input`)
.exists("renders the dashed custom field without crashing");
});
test("clears livestream when the location stops being a URL", async function (assert) {
this.siteSettings.chat_enabled = true;
const event = DiscoursePostEventEvent.create({
name: "My event",
starts_at: "2022-07-01T10:00:00Z",
ends_at: "2022-07-01T11:00:00Z",
timezone: "UTC",
status: "public",
reminders: [],
raw_invitees: [],
custom_fields: {},
location: "https://zoom.us/j/123456789",
livestream: true,
});
const model = {
event,
initialScreen: "advanced",
onUpdate: () => {},
toolbarEvent: {},
};
const closeModal = () => {};
await render(
<template>
<PostEventBuilder
@inline={{true}}
@model={{model}}
@closeModal={{closeModal}}
/>
</template>
);
assert
.dom(`[data-name="livestream"]`)
.exists("the livestream field shows while the location is a URL");
await fillIn(`[data-name="location"] input`, "Room 5");
assert
.dom(`[data-name="livestream"]`)
.doesNotExist("the livestream field is hidden for a non-URL location");
assert.false(
event.livestream,
"livestream is reset so it is not submitted for a non-URL location"
);
});
});