Skip to content
This repository was archived by the owner on Mar 5, 2025. It is now read-only.

Commit 52f7ab8

Browse files
committed
Implement the widget lifecycle module
Signed-off-by: Dominik Henneke <[email protected]>
1 parent 3455fee commit 52f7ab8

23 files changed

+987
-18
lines changed

e2e/src/deploy/elementWeb/config.json

+11
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,16 @@
4848
"guest_user_homeserver_url": "{{HOMESERVER_URL}}",
4949
"skip_single_sign_on": true
5050
}
51+
},
52+
"net.nordeck.element_web.module.widget_lifecycle": {
53+
"widget_permissions": {
54+
"{{WIDGET_SERVER_URL}}/widget.html": {
55+
"preload_approved": true,
56+
"identity_approved": true,
57+
"capabilities_approved": [
58+
"org.matrix.msc2762.receive.state_event:m.room.topic"
59+
]
60+
}
61+
}
5162
}
5263
}

e2e/src/deploy/elementWeb/index.ts

+5-4
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ let container: StartedTestContainer | undefined;
2121

2222
export async function startElementWeb({
2323
homeserverUrl,
24+
widgetServerUrl,
2425
version = 'v1.11.40',
2526
}: {
2627
homeserverUrl: string;
28+
widgetServerUrl: string;
2729
version?: string;
2830
}): Promise<{ elementWebUrl: string }> {
2931
console.log(`Starting element web… (version ${version})`);
@@ -32,10 +34,9 @@ export async function startElementWeb({
3234
require.resolve('./config.json'),
3335
'utf-8',
3436
);
35-
const elementWebConfig = elementWebConfigTemplate.replace(
36-
/{{HOMESERVER_URL}}/g,
37-
homeserverUrl,
38-
);
37+
const elementWebConfig = elementWebConfigTemplate
38+
.replace(/{{HOMESERVER_URL}}/g, homeserverUrl)
39+
.replace(/{{WIDGET_SERVER_URL}}/g, widgetServerUrl);
3940

4041
const elementContainer = await GenericContainer.fromDockerfile(__dirname)
4142
.withBuildArgs({ ELEMENT_VERSION: version })

e2e/src/deploy/setup.ts

+7
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import { FullConfig } from '@playwright/test';
1818
import { startElementWeb } from './elementWeb';
1919
import { startSynapse } from './synapse';
20+
import { startWidgetServer } from './widgets';
2021

2122
export default async function globalSetup(_config: FullConfig) {
2223
const { synapseUrl, registrationSecret } = await startSynapse({
@@ -25,8 +26,14 @@ export default async function globalSetup(_config: FullConfig) {
2526
process.env.SYNAPSE_URL = synapseUrl;
2627
process.env.SYNAPSE_REGISTRATION_SECRET = registrationSecret;
2728

29+
const { widgetServerUrl } = await startWidgetServer({
30+
homeserverUrl: synapseUrl,
31+
});
32+
process.env.WIDGET_SERVER_URL = widgetServerUrl;
33+
2834
const { elementWebUrl } = await startElementWeb({
2935
homeserverUrl: synapseUrl,
36+
widgetServerUrl,
3037
});
3138
process.env.ELEMENT_WEB_URL = elementWebUrl;
3239
}

e2e/src/deploy/teardown.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
import { FullConfig } from '@playwright/test';
1818
import { stopElementWeb } from './elementWeb';
1919
import { stopSynapse } from './synapse';
20+
import { stopWidgetServer } from './widgets';
2021

2122
export default async function globalTeardown(_config: FullConfig) {
22-
await Promise.all([stopSynapse(), stopElementWeb()]);
23+
await Promise.all([stopSynapse(), stopWidgetServer(), stopElementWeb()]);
2324
}

e2e/src/deploy/widgets/index.ts

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2023 Nordeck IT + Consulting GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { readFile } from 'fs/promises';
18+
import * as http from 'http';
19+
import { AddressInfo } from 'net';
20+
21+
let server: http.Server;
22+
23+
export async function startWidgetServer({
24+
homeserverUrl,
25+
}: {
26+
homeserverUrl: string;
27+
}): Promise<{
28+
widgetServerUrl: string;
29+
}> {
30+
console.log(`Starting widget server…`);
31+
32+
const widgetHtmlTemplate = await readFile(
33+
require.resolve('./widget.html'),
34+
'utf-8',
35+
);
36+
37+
const widgetHtml = widgetHtmlTemplate.replace(
38+
/{{HOMESERVER_URL}}/g,
39+
homeserverUrl,
40+
);
41+
42+
server = http.createServer((_, res) => {
43+
res.writeHead(200, { 'Content-Type': 'text/html' });
44+
res.end(widgetHtml);
45+
});
46+
server.listen();
47+
48+
const widgetServerUrl = `http://localhost:${
49+
(server.address() as AddressInfo).port
50+
}`;
51+
console.log('Widget server running at', widgetServerUrl);
52+
53+
return { widgetServerUrl };
54+
}
55+
56+
export async function stopWidgetServer() {
57+
if (server) {
58+
server.close();
59+
console.log('Stopped widget server');
60+
}
61+
}

e2e/src/deploy/widgets/widget.html

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<html lang="en">
2+
<head>
3+
<title>Demo Widget</title>
4+
<script>
5+
let sendEventCount = 0;
6+
window.onmessage = async (ev) => {
7+
if (ev.data.action === 'capabilities') {
8+
// Return the capabilities for this widget
9+
window.parent.postMessage(
10+
Object.assign(
11+
{
12+
response: {
13+
capabilities: [
14+
'org.matrix.msc2762.receive.state_event:m.room.topic',
15+
],
16+
},
17+
},
18+
ev.data,
19+
),
20+
'*',
21+
);
22+
} else if (ev.data.action === 'notify_capabilities') {
23+
// Ask for an openid token
24+
window.parent.postMessage(
25+
{
26+
api: 'fromWidget',
27+
widgetId: ev.data.widgetId,
28+
requestId: 'widget-' + sendEventCount,
29+
action: 'get_openid',
30+
data: {},
31+
},
32+
'*',
33+
);
34+
} else if (
35+
ev.data.action === 'get_openid' &&
36+
ev.data.response?.state === 'allowed'
37+
) {
38+
// Add the userid to the heading
39+
const { matrix_server_name, access_token } = ev.data.response;
40+
41+
const response = await fetch(
42+
`{{HOMESERVER_URL}}/_matrix/federation/v1/openid/userinfo?access_token=${access_token}`,
43+
);
44+
const { sub } = await response.json();
45+
46+
const titleElement = document.getElementById('title');
47+
titleElement.innerText = `Hello ${sub}!`;
48+
}
49+
};
50+
</script>
51+
</head>
52+
<body>
53+
<h1 id="title">Hello unknown!</h1>
54+
</body>
55+
</html>

e2e/src/fixtures.ts

+44
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ type Fixtures = {
2424
alicePage: Page;
2525
aliceElementWebPage: ElementWebPage;
2626
bob: User;
27+
bobPage: Page;
28+
bobElementWebPage: ElementWebPage;
2729
guestPage: Page;
2830
guestElementWebPage: ElementWebPage;
2931
runAxeAnalysis: (page: Page) => Promise<string>;
@@ -65,6 +67,48 @@ export const test = base.extend<Fixtures>({
6567
await use(user);
6668
},
6769

70+
bobPage: async ({ browser, contextOptions, video }, use, testInfo) => {
71+
// TODO: For some reason we are missing the video in case we are using a
72+
// second context https://github.com/microsoft/playwright/issues/9002
73+
// We configure it manually instead.
74+
const videoMode = typeof video === 'string' ? video : video.mode;
75+
const videoOptions = shouldCaptureVideo(videoMode, testInfo)
76+
? {
77+
recordVideo: {
78+
dir: testInfo.outputDir,
79+
size: typeof video !== 'string' ? video.size : undefined,
80+
},
81+
}
82+
: {};
83+
84+
const context = await browser.newContext({
85+
...contextOptions,
86+
...videoOptions,
87+
});
88+
const page = await context.newPage();
89+
90+
try {
91+
await use(page);
92+
} finally {
93+
await context.close();
94+
95+
const video = page.video();
96+
97+
if (video) {
98+
const path = testInfo.outputPath('video-bob.webm');
99+
await video.saveAs(path);
100+
testInfo.attach('video', { path, contentType: 'video/webm' });
101+
}
102+
}
103+
},
104+
105+
bobElementWebPage: async ({ bobPage, bob }, use) => {
106+
const elementWebPage = new ElementWebPage(bobPage);
107+
await elementWebPage.login(bob.username, bob.password);
108+
109+
await use(elementWebPage);
110+
},
111+
68112
guestPage: async ({ browser, contextOptions, video }, use, testInfo) => {
69113
// TODO: For some reason we are missing the video in case we are using a
70114
// second context https://github.com/microsoft/playwright/issues/9002

e2e/src/pages/elementWebPage.ts

+36-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { Locator, Page } from '@playwright/test';
17+
import { FrameLocator, Locator, Page } from '@playwright/test';
1818
import fetch from 'cross-fetch';
1919
import { Credentials, getElementWebUrl, getSynapseUrl } from '../util';
2020
import { CreateDirectMessagePage } from './createDirectMessagePage';
@@ -25,6 +25,7 @@ export class ElementWebPage {
2525
private readonly navigationRegion: Locator;
2626
private readonly mainRegion: Locator;
2727
private readonly headerRegion: Locator;
28+
private readonly sendMessageTextbox: Locator;
2829
private readonly roomNameText: Locator;
2930
private readonly userMenuButton: Locator;
3031
private readonly startChatButton: Locator;
@@ -33,6 +34,7 @@ export class ElementWebPage {
3334
this.navigationRegion = page.getByRole('navigation');
3435
this.mainRegion = page.getByRole('main');
3536
this.headerRegion = this.mainRegion.locator('header');
37+
this.sendMessageTextbox = page.getByRole('textbox', { name: /message/ });
3638
this.roomNameText = this.headerRegion.getByRole('heading');
3739
this.userMenuButton = this.navigationRegion.getByRole('button', {
3840
name: 'User menu',
@@ -113,12 +115,45 @@ export class ElementWebPage {
113115
await this.waitForRoom(name);
114116
}
115117

118+
async acceptRoomInvitation() {
119+
await this.page.getByRole('button', { name: 'Accept' }).click();
120+
}
121+
116122
async joinRoom() {
117123
await this.page
118124
.getByRole('button', { name: 'Join the discussion' })
119125
.click();
120126
}
121127

128+
async toggleRoomInfo() {
129+
await this.headerRegion.getByRole('button', { name: 'Room info' }).click();
130+
}
131+
132+
async sendMessage(message: string) {
133+
// Both for encrypted and non-encrypted cases
134+
await this.sendMessageTextbox.type(message);
135+
await this.sendMessageTextbox.press('Enter');
136+
}
137+
138+
widgetByTitle(title: string): FrameLocator {
139+
return this.page.frameLocator(`iframe[title="${title}"]`);
140+
}
141+
142+
async setupWidget(url: string) {
143+
await this.sendMessage(`/addwidget ${url}`);
144+
145+
await this.toggleRoomInfo();
146+
await this.page
147+
.getByRole('button', { name: 'Custom' })
148+
.locator('..')
149+
.getByRole('button', { name: 'Pin' })
150+
.click();
151+
152+
await this.page
153+
.getByRole('button', { name: 'Set my room layout for everyone' })
154+
.click();
155+
}
156+
122157
async inviteUser(username: string) {
123158
const roomId = this.getCurrentRoomId();
124159

e2e/src/util/config.ts

+8
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,14 @@
1414
* limitations under the License.
1515
*/
1616

17+
export function getWidgetServerUrl(): string {
18+
if (!process.env.WIDGET_SERVER_URL) {
19+
throw new Error('WIDGET_SERVER_URL unavailable');
20+
}
21+
22+
return process.env.WIDGET_SERVER_URL;
23+
}
24+
1725
export function getElementWebUrl(): string {
1826
if (!process.env.ELEMENT_WEB_URL) {
1927
throw new Error('ELEMENT_WEB_URL unavailable');

e2e/src/widgetLifecycle.spec.ts

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/*
2+
* Copyright 2023 Nordeck IT + Consulting GmbH
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { expect } from '@playwright/test';
18+
import { test } from './fixtures';
19+
import { getWidgetServerUrl } from './util/config';
20+
21+
test.describe('Widget Lifecycle Module', () => {
22+
test('should show the widget without any permission requests', async ({
23+
aliceElementWebPage,
24+
bob,
25+
bobElementWebPage,
26+
}) => {
27+
await aliceElementWebPage.createRoom('Trusted Widget');
28+
await aliceElementWebPage.inviteUser(bob.username);
29+
await aliceElementWebPage.setupWidget(
30+
getWidgetServerUrl().concat('/widget.html'),
31+
);
32+
33+
await bobElementWebPage.navigateToRoomOrInvitation('Trusted Widget');
34+
await bobElementWebPage.acceptRoomInvitation();
35+
36+
await expect(
37+
bobElementWebPage
38+
.widgetByTitle('Custom')
39+
.getByRole('heading', { name: `Hello ${bob.credentials.userId}!` }),
40+
).toBeVisible();
41+
});
42+
});

0 commit comments

Comments
 (0)