Skip to content

Commit 17083ed

Browse files
committed
Add some tests so that we detect multiplex is no more working
they use playwright and a local multiplex server
1 parent bb0578d commit 17083ed

File tree

9 files changed

+326
-7
lines changed

9 files changed

+326
-7
lines changed

.github/workflows/test-smokes.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,15 @@ jobs:
8383
with:
8484
node-version: 20
8585

86+
- name: Cache multiplex server node_modules
87+
if: ${{ runner.os != 'Windows' || github.event_name == 'schedule' }}
88+
uses: actions/cache@v4
89+
with:
90+
path: tests/integration/playwright/multiplex-server/node_modules
91+
key: ${{ runner.os }}-multiplex-server-${{ hashFiles('tests/integration/playwright/multiplex-server/package.json') }}
92+
restore-keys: |
93+
${{ runner.os }}-multiplex-server-
94+
8695
- name: Install node dependencies
8796
if: ${{ runner.os != 'Windows' || github.event_name == 'schedule' }}
8897
run: yarn
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
---
2+
title: "Multiplex Test Presentation"
3+
format:
4+
revealjs:
5+
multiplex:
6+
url: "http://127.0.0.1:1948/"
7+
# setting secret and id is required to avoid having multiplex server running at render time
8+
secret: "c04998070a4ec17940ab3c52101daefd"
9+
id: "52d3aedefbff55dbe54e1fa5229df99b849a33951c7c09afb6bcf7e6f33233c7"
10+
---
11+
12+
## Slide 1 {#slide-1}
13+
14+
This is the first slide.
15+
16+
## Slide 2 {#slide-2}
17+
18+
This is the second slide with some content.
19+
20+
## Slide 3 {#slide-3}
21+
22+
### Fragment Test
23+
24+
::: {.fragment}
25+
Fragment 1
26+
:::
27+
28+
::: {.fragment}
29+
Fragment 2
30+
:::
31+
32+
## Slide 4 {#slide-4}
33+
34+
Final slide with more content.

tests/integration/playwright-tests.test.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import { execProcess } from "../../src/core/process.ts";
1616
import { quartoDevCmd } from "../utils.ts";
1717
import { fail } from "testing/asserts";
1818
import { isWindows } from "../../src/deno_ral/platform.ts";
19+
import { join } from "../../src/deno_ral/path.ts";
20+
import { existsSync } from "../../src/deno_ral/fs.ts";
1921

2022
async function fullInit() {
2123
await initYamlIntelligenceResourcesFromFilesystem();
@@ -30,6 +32,19 @@ const globOutput = Deno.args.length
3032
setInitializer(fullInit);
3133
await initState();
3234

35+
// Install multiplex server dependencies if needed
36+
const multiplexServerPath = "integration/playwright/multiplex-server";
37+
const multiplexNodeModules = join(multiplexServerPath, "node_modules");
38+
if (!existsSync(multiplexNodeModules)) {
39+
console.log("Installing multiplex server dependencies...");
40+
await execProcess({
41+
cmd: isWindows ? "npm.cmd" : "npm",
42+
args: ["install", "--loglevel=error"],
43+
cwd: multiplexServerPath,
44+
});
45+
console.log("Multiplex server dependencies installed.");
46+
}
47+
3348
// const promises = [];
3449
const fileNames: string[] = [];
3550
const extraOpts = [
@@ -52,7 +67,7 @@ for (const { path: fileName } of globOutput) {
5267
// mediabag inspection if we don't wait all renders
5368
// individually. This is very slow..
5469
await execProcess({
55-
cmd: quartoDevCmd(),
70+
cmd: quartoDevCmd(),
5671
args: ["render", input, ...options],
5772
});
5873
fileNames.push(fileName);
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules/
2+
package-lock.json
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# RevealJS Multiplex Server (Local Test Instance)
2+
3+
This is a local instance of the [reveal/multiplex](https://github.com/reveal/multiplex) server used for testing Quarto's RevealJS multiplex feature.
4+
5+
## Purpose
6+
7+
This server enables testing of the multiplex presentation feature without relying on external services. It:
8+
9+
- Generates tokens (secret/socketId pairs)
10+
- Manages socket.io connections between master and client presentations
11+
- Broadcasts presentation state changes from master to clients
12+
13+
## Usage
14+
15+
The server is automatically started by Playwright's `webServer` configuration when running tests. It listens on `http://localhost:1948` by default.
16+
17+
## Installation
18+
19+
Dependencies are automatically installed by `tests/integration/playwright-tests.test.ts` before running Playwright tests.
20+
21+
Manual installation:
22+
```bash
23+
npm install
24+
```
25+
26+
## Running Manually
27+
28+
```bash
29+
npm start
30+
```
31+
32+
## Attribution
33+
34+
Server code is from the [reveal/multiplex](https://github.com/reveal/multiplex) repository.
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
let http = require("http");
2+
let express = require("express");
3+
let cors = require("cors");
4+
let fs = require("fs");
5+
let io = require("socket.io");
6+
let crypto = require("crypto");
7+
8+
let app = express();
9+
let staticDir = express.static;
10+
11+
app.use(cors()); // enable cors for all origins
12+
13+
let server = http.createServer(app);
14+
15+
let socketsIO = io(server, {
16+
cors: {
17+
origin: "*",
18+
methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
19+
},
20+
});
21+
22+
let opts = {
23+
port: process.env.PORT || 1948,
24+
baseDir: process.cwd(),
25+
};
26+
27+
socketsIO.on("connection", (socket) => {
28+
console.debug("Connection opened");
29+
socket.on("multiplex-statechanged", (data) => {
30+
if (
31+
typeof data.secret == "undefined" ||
32+
data.secret == null ||
33+
data.secret === ""
34+
)
35+
return;
36+
if (createHash(data.secret) === data.socketId) {
37+
console.debug("Broadcasting state change");
38+
data.secret = null;
39+
socket.broadcast.emit(data.socketId, data);
40+
} else {
41+
console.warn("Secret and socketId do not match");
42+
}
43+
});
44+
});
45+
46+
app.use(express.static(opts.baseDir));
47+
48+
app.get("/", (req, res) => {
49+
res.writeHead(200, { "Content-Type": "text/html" });
50+
51+
let stream = fs.createReadStream(opts.baseDir + "/index.html");
52+
stream.on("error", (error) => {
53+
res.write(
54+
'<style>body{font-family: sans-serif;}</style><h2>reveal.js multiplex server.</h2><a href="/token">Generate token</a>'
55+
);
56+
res.end();
57+
});
58+
stream.on("open", () => {
59+
stream.pipe(res);
60+
});
61+
});
62+
63+
app.get("/token", (req, res) => {
64+
let secret = crypto.randomBytes(16).toString("hex");
65+
res.send({ secret: secret, socketId: createHash(secret) });
66+
});
67+
68+
let createHash = (secret) => {
69+
let hash = crypto.createHash("sha256").update(secret);
70+
return hash.digest("hex");
71+
};
72+
73+
// Open the listening port
74+
server.listen(opts.port || null);
75+
76+
console.log(`reveal.js: Multiplex running on port: ${opts.port}`);
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "reveal-multiplex",
3+
"version": "0.1.0",
4+
"description": "reveal.js multiplex plugin - local test server",
5+
"homepage": "https://revealjs.com",
6+
"scripts": {
7+
"start": "node index.js"
8+
},
9+
"engines": {
10+
"node": ">=18.0.0"
11+
},
12+
"dependencies": {
13+
"cors": "^2.8.5",
14+
"express": "~4.17.1",
15+
"mustache": "~4.0.0",
16+
"socket.io": "^2.5.0"
17+
},
18+
"license": "MIT"
19+
}

tests/integration/playwright/playwright.config.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -94,10 +94,21 @@ export default defineConfig({
9494

9595
/* Run your local dev server before starting the tests */
9696
/* We use python for this but we could also try using another tool */
97-
webServer: {
98-
command: 'uv run python -m http.server 8080',
99-
url: 'http://127.0.0.1:8080',
100-
reuseExistingServer: !isCI,
101-
cwd: '../../docs/playwright',
102-
},
97+
webServer: [
98+
{
99+
// HTTP server for rendered HTML files
100+
command: 'uv run python -m http.server 8080',
101+
url: 'http://127.0.0.1:8080',
102+
reuseExistingServer: !isCI,
103+
cwd: '../../docs/playwright',
104+
},
105+
{
106+
// Socket.IO multiplex server for RevealJS
107+
command: 'npm start',
108+
url: 'http://127.0.0.1:1948',
109+
reuseExistingServer: !isCI,
110+
cwd: './multiplex-server',
111+
timeout: 10000,
112+
}
113+
],
103114
});
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { test, expect, Page, Browser, BrowserContext } from '@playwright/test';
2+
3+
async function getRevealState(page: Page) {
4+
return await page.evaluate(() => {
5+
const reveal = (window as any).Reveal;
6+
return reveal ? reveal.getState() : null;
7+
});
8+
}
9+
10+
const SOCKET_PROPAGATION_DELAY = 500;
11+
12+
async function createMultiplexPage(browser: Browser, url: string) {
13+
const context = await browser.newContext();
14+
const page = await context.newPage();
15+
await page.goto(url);
16+
return { context, page };
17+
}
18+
19+
/**
20+
* Helper to create both master and client pages for multiplex testing
21+
*/
22+
async function createMultiplexPages(browser: Browser) {
23+
const master = await createMultiplexPage(browser, './revealjs/multiplex-speaker.html');
24+
const client = await createMultiplexPage(browser, './revealjs/multiplex.html');
25+
return { master, client };
26+
}
27+
28+
async function expectSlidePositions(
29+
master: Page,
30+
client: Page,
31+
expectedMasterH: number,
32+
expectedClientH: number,
33+
expectedMasterV?: number,
34+
expectedClientV?: number
35+
) {
36+
const masterState = await getRevealState(master);
37+
const clientState = await getRevealState(client);
38+
39+
expect(masterState.indexh).toBe(expectedMasterH);
40+
expect(clientState.indexh).toBe(expectedClientH);
41+
42+
if (expectedMasterV !== undefined) {
43+
expect(masterState.indexv).toBe(expectedMasterV);
44+
}
45+
if (expectedClientV !== undefined) {
46+
expect(clientState.indexv).toBe(expectedClientV);
47+
}
48+
}
49+
50+
async function expectFragmentPositions(
51+
master: Page,
52+
client: Page,
53+
expectedMasterF: number,
54+
expectedClientF: number
55+
) {
56+
const masterState = await getRevealState(master);
57+
const clientState = await getRevealState(client);
58+
59+
expect(masterState.indexf).toBe(expectedMasterF);
60+
expect(clientState.indexf).toBe(expectedClientF);
61+
}
62+
63+
/**
64+
* Test the multiplex feature where a master presentation controls client presentations.
65+
* Tests: slide synchronization, fragment synchronization, and unidirectional control.
66+
*/
67+
test('multiplex: master controls client, fragments sync, and client cannot control master', async ({ browser }) => {
68+
const { master, client } = await createMultiplexPages(browser);
69+
70+
try {
71+
// Both should start on title slide
72+
await expectSlidePositions(master.page, client.page, 0, 0, 0, 0);
73+
74+
// Test 1: Basic slide synchronization
75+
await master.page.keyboard.press('ArrowRight');
76+
await master.page.waitForTimeout(SOCKET_PROPAGATION_DELAY);
77+
await expectSlidePositions(master.page, client.page, 1, 1);
78+
79+
await master.page.keyboard.press('ArrowRight');
80+
await master.page.waitForTimeout(SOCKET_PROPAGATION_DELAY);
81+
await expectSlidePositions(master.page, client.page, 2, 2);
82+
83+
// Test 2: Fragment synchronization on slide 3
84+
await master.page.keyboard.press('ArrowRight'); // slide 2 -> slide 3
85+
await master.page.waitForTimeout(SOCKET_PROPAGATION_DELAY);
86+
await expectSlidePositions(master.page, client.page, 3, 3);
87+
88+
// Show first fragment
89+
await master.page.keyboard.press('ArrowRight');
90+
await master.page.waitForTimeout(SOCKET_PROPAGATION_DELAY);
91+
await expectFragmentPositions(master.page, client.page, 0, 0);
92+
93+
// Show second fragment
94+
await master.page.keyboard.press('ArrowRight');
95+
await master.page.waitForTimeout(SOCKET_PROPAGATION_DELAY);
96+
await expectFragmentPositions(master.page, client.page, 1, 1);
97+
98+
// Test 3: Client cannot control master
99+
// Navigate back to title slide
100+
await master.page.goto('./revealjs/multiplex-speaker.html#/');
101+
await master.page.waitForTimeout(SOCKET_PROPAGATION_DELAY);
102+
await expectSlidePositions(master.page, client.page, 0, 0);
103+
104+
// Client tries to navigate (should not affect master)
105+
await client.page.keyboard.press('ArrowRight');
106+
await client.page.waitForTimeout(SOCKET_PROPAGATION_DELAY);
107+
await expectSlidePositions(master.page, client.page, 0, 1);
108+
109+
// Master overrides client's position
110+
await master.page.keyboard.press('ArrowRight');
111+
await master.page.keyboard.press('ArrowRight');
112+
await master.page.waitForTimeout(SOCKET_PROPAGATION_DELAY);
113+
await expectSlidePositions(master.page, client.page, 2, 2);
114+
115+
} finally {
116+
await master.context.close();
117+
await client.context.close();
118+
}
119+
});

0 commit comments

Comments
 (0)