Add integration test for call decline
This commit is contained in:
parent
cf03754d2f
commit
b95161859e
7 changed files with 158 additions and 10 deletions
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
|
@ -429,7 +429,8 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
xvfb-run --auto-servernum pnpm run test-mock
|
xvfb-run --auto-servernum pnpm run test-mock
|
||||||
timeout-minutes: 10
|
xvfb-run --auto-servernum pnpm run test-mock-docker
|
||||||
|
timeout-minutes: 15
|
||||||
env:
|
env:
|
||||||
NODE_ENV: production
|
NODE_ENV: production
|
||||||
DEBUG: mock:test:*
|
DEBUG: mock:test:*
|
||||||
|
|
|
@ -48,6 +48,7 @@
|
||||||
"test-release": "node ts/scripts/test-release.js",
|
"test-release": "node ts/scripts/test-release.js",
|
||||||
"test-node": "cross-env LANG=en-us electron-mocha --timeout 10000 --main test/fix-linux-gtk.js --file test/setup-test-node.js --recursive ts/test-node",
|
"test-node": "cross-env LANG=en-us electron-mocha --timeout 10000 --main test/fix-linux-gtk.js --file test/setup-test-node.js --recursive ts/test-node",
|
||||||
"test-mock": "mocha --require ts/test-mock/setup-ci.js ts/test-mock/**/*_test.js",
|
"test-mock": "mocha --require ts/test-mock/setup-ci.js ts/test-mock/**/*_test.js",
|
||||||
|
"test-mock-docker": "mocha --require ts/test-mock/setup-ci.js ts/test-mock/**/*_test.docker.js",
|
||||||
"test-eslint": "mocha .eslint/rules/**/*.test.js --ignore-leaks",
|
"test-eslint": "mocha .eslint/rules/**/*.test.js --ignore-leaks",
|
||||||
"test-lint-intl": "ts-node ./build/intl-linter/linter.ts --test",
|
"test-lint-intl": "ts-node ./build/intl-linter/linter.ts --test",
|
||||||
"eslint": "eslint --cache . --cache-strategy content --max-warnings 0",
|
"eslint": "eslint --cache . --cache-strategy content --max-warnings 0",
|
||||||
|
@ -222,7 +223,7 @@
|
||||||
"@indutny/parallel-prettier": "3.0.0",
|
"@indutny/parallel-prettier": "3.0.0",
|
||||||
"@indutny/rezip-electron": "2.0.1",
|
"@indutny/rezip-electron": "2.0.1",
|
||||||
"@napi-rs/canvas": "0.1.61",
|
"@napi-rs/canvas": "0.1.61",
|
||||||
"@signalapp/mock-server": "13.1.0",
|
"@signalapp/mock-server": "13.2.0",
|
||||||
"@storybook/addon-a11y": "8.4.4",
|
"@storybook/addon-a11y": "8.4.4",
|
||||||
"@storybook/addon-actions": "8.4.4",
|
"@storybook/addon-actions": "8.4.4",
|
||||||
"@storybook/addon-controls": "8.4.4",
|
"@storybook/addon-controls": "8.4.4",
|
||||||
|
|
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
|
@ -430,8 +430,8 @@ importers:
|
||||||
specifier: 0.1.61
|
specifier: 0.1.61
|
||||||
version: 0.1.61
|
version: 0.1.61
|
||||||
'@signalapp/mock-server':
|
'@signalapp/mock-server':
|
||||||
specifier: 13.1.0
|
specifier: 13.2.0
|
||||||
version: 13.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)
|
version: 13.2.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)
|
||||||
'@storybook/addon-a11y':
|
'@storybook/addon-a11y':
|
||||||
specifier: 8.4.4
|
specifier: 8.4.4
|
||||||
version: 8.4.4(storybook@8.4.4(bufferutil@4.0.9)(prettier@3.3.3)(utf-8-validate@5.0.10))
|
version: 8.4.4(storybook@8.4.4(bufferutil@4.0.9)(prettier@3.3.3)(utf-8-validate@5.0.10))
|
||||||
|
@ -2770,8 +2770,8 @@ packages:
|
||||||
'@signalapp/libsignal-client@0.76.3':
|
'@signalapp/libsignal-client@0.76.3':
|
||||||
resolution: {integrity: sha512-Ht8XtdsSvgiCb8ftUYE9DaLcWy0vltrj9cQ2sfy+DGUayE1k2njicNhB2RKOfQV2Wb/1Cl0WxVZP/NlXRo2+jA==}
|
resolution: {integrity: sha512-Ht8XtdsSvgiCb8ftUYE9DaLcWy0vltrj9cQ2sfy+DGUayE1k2njicNhB2RKOfQV2Wb/1Cl0WxVZP/NlXRo2+jA==}
|
||||||
|
|
||||||
'@signalapp/mock-server@13.1.0':
|
'@signalapp/mock-server@13.2.0':
|
||||||
resolution: {integrity: sha512-CuDNLNEBMzwIs5jr7Lx9F4YFoRD62s7WgPGtm3qpaggixSQtabjMC7AKSR0xvaHcZpYZtBU5jcGK8Roguo9nuw==}
|
resolution: {integrity: sha512-f5uxzsIwPmkevX5ycCRWgqy/VCbvj/dUA8yGWlfO2hFc62UZ4FaOY0n5YFMOpQgp5eMO6TMgr1KqIR/uAxfkIg==}
|
||||||
|
|
||||||
'@signalapp/parchment-cjs@3.0.1':
|
'@signalapp/parchment-cjs@3.0.1':
|
||||||
resolution: {integrity: sha512-hSBMQ1M7wE4GcC8ZeNtvpJF+DAJg3eIRRf1SiHS3I3Algav/sgJJNm6HIYm6muHuK7IJmuEjkL3ILSXgmu0RfQ==}
|
resolution: {integrity: sha512-hSBMQ1M7wE4GcC8ZeNtvpJF+DAJg3eIRRf1SiHS3I3Algav/sgJJNm6HIYm6muHuK7IJmuEjkL3ILSXgmu0RfQ==}
|
||||||
|
@ -12466,7 +12466,7 @@ snapshots:
|
||||||
type-fest: 4.26.1
|
type-fest: 4.26.1
|
||||||
uuid: 11.0.2
|
uuid: 11.0.2
|
||||||
|
|
||||||
'@signalapp/mock-server@13.1.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)':
|
'@signalapp/mock-server@13.2.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@indutny/parallel-prettier': 3.0.0(prettier@3.3.3)
|
'@indutny/parallel-prettier': 3.0.0(prettier@3.3.3)
|
||||||
'@signalapp/libsignal-client': 0.60.2
|
'@signalapp/libsignal-client': 0.60.2
|
||||||
|
|
110
ts/test-mock/calling/callMessages_test.docker.ts
Normal file
110
ts/test-mock/calling/callMessages_test.docker.ts
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
// Copyright 2025 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import { StorageState } from '@signalapp/mock-server';
|
||||||
|
import { expect } from 'playwright/test';
|
||||||
|
import * as durations from '../../util/durations';
|
||||||
|
import type { App } from '../playwright';
|
||||||
|
import { Bootstrap } from '../bootstrap';
|
||||||
|
import { runTurnInContainer, tearDownTurnContainer } from './helpers';
|
||||||
|
|
||||||
|
describe('callMessages', function callMessages(this: Mocha.Suite) {
|
||||||
|
this.timeout(durations.MINUTE);
|
||||||
|
|
||||||
|
let bootstrap1: Bootstrap;
|
||||||
|
let bootstrap2: Bootstrap;
|
||||||
|
let app1: App;
|
||||||
|
let app2: App;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
runTurnInContainer();
|
||||||
|
|
||||||
|
bootstrap1 = new Bootstrap();
|
||||||
|
await bootstrap1.init();
|
||||||
|
|
||||||
|
bootstrap2 = new Bootstrap({ server: bootstrap1.server });
|
||||||
|
await bootstrap2.init();
|
||||||
|
|
||||||
|
let state1 = StorageState.getEmpty();
|
||||||
|
state1 = state1.updateAccount({
|
||||||
|
profileKey: bootstrap1.phone.profileKey.serialize(),
|
||||||
|
});
|
||||||
|
|
||||||
|
state1 = state1.addContact(bootstrap2.phone, {
|
||||||
|
whitelisted: true,
|
||||||
|
profileKey: bootstrap2.phone.profileKey.serialize(),
|
||||||
|
givenName: 'Contact2',
|
||||||
|
});
|
||||||
|
|
||||||
|
state1 = state1.pin(bootstrap2.phone);
|
||||||
|
|
||||||
|
await bootstrap1.phone.setStorageState(state1);
|
||||||
|
|
||||||
|
app1 = await bootstrap1.link();
|
||||||
|
|
||||||
|
let state2 = StorageState.getEmpty();
|
||||||
|
state2 = state2.updateAccount({
|
||||||
|
profileKey: bootstrap2.phone.profileKey.serialize(),
|
||||||
|
});
|
||||||
|
|
||||||
|
state2 = state2.addContact(bootstrap1.phone, {
|
||||||
|
whitelisted: true,
|
||||||
|
profileKey: bootstrap1.phone.profileKey.serialize(),
|
||||||
|
givenName: 'Contact1',
|
||||||
|
});
|
||||||
|
|
||||||
|
state2 = state2.pin(bootstrap1.phone);
|
||||||
|
await bootstrap2.phone.setStorageState(state2);
|
||||||
|
|
||||||
|
app2 = await bootstrap2.link();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async function after(this: Mocha.Context) {
|
||||||
|
tearDownTurnContainer();
|
||||||
|
|
||||||
|
if (!bootstrap1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await bootstrap1.maybeSaveLogs(this.currentTest, app1);
|
||||||
|
await bootstrap2.maybeSaveLogs(this.currentTest, app2);
|
||||||
|
|
||||||
|
await app2.close();
|
||||||
|
await app1.close();
|
||||||
|
|
||||||
|
await bootstrap2.teardown();
|
||||||
|
await bootstrap1.teardown();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can call and decline a call', async () => {
|
||||||
|
const window1 = await app1.getWindow();
|
||||||
|
const leftPane1 = window1.locator('#LeftPane');
|
||||||
|
|
||||||
|
await leftPane1
|
||||||
|
.locator(`[data-testid="${bootstrap2.phone.device.aci}"]`)
|
||||||
|
.click();
|
||||||
|
// Try to start a call
|
||||||
|
await window1.locator('.module-ConversationHeader__button--audio').click();
|
||||||
|
const window1Permissions = await app1.waitForWindow();
|
||||||
|
await window1Permissions.getByText('Allow Access').click();
|
||||||
|
await window1
|
||||||
|
.locator('.CallingLobbyJoinButton')
|
||||||
|
.and(window1.locator('button:visible'))
|
||||||
|
.click();
|
||||||
|
|
||||||
|
const window2 = await app2.getWindow();
|
||||||
|
|
||||||
|
// Only wait for 3 seconds to make sure that this succeeded properly rather
|
||||||
|
// than timing out after ~10 seconds and using a direct connection
|
||||||
|
await window2
|
||||||
|
.locator('.IncomingCallBar__button--decline')
|
||||||
|
.click({ timeout: 3000 });
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
window1.locator('.module-calling__modal-container')
|
||||||
|
).toBeEmpty();
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
window2.locator('.module-calling__modal-container')
|
||||||
|
).toBeEmpty();
|
||||||
|
});
|
||||||
|
});
|
32
ts/test-mock/calling/helpers.ts
Normal file
32
ts/test-mock/calling/helpers.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// Copyright 2025 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
import * as child from 'child_process';
|
||||||
|
import createDebug from 'debug';
|
||||||
|
|
||||||
|
const debug = createDebug('mock:test:calling:helpers');
|
||||||
|
|
||||||
|
export function runTurnInContainer(): void {
|
||||||
|
tearDownTurnContainer();
|
||||||
|
const result = child.spawnSync('docker', [
|
||||||
|
'run',
|
||||||
|
'--name',
|
||||||
|
'coturn',
|
||||||
|
'-d',
|
||||||
|
'--network=host',
|
||||||
|
'coturn/coturn',
|
||||||
|
]);
|
||||||
|
debug(
|
||||||
|
'create coturn: signal: ',
|
||||||
|
result.signal,
|
||||||
|
' status: ',
|
||||||
|
result.status,
|
||||||
|
'stderr: ',
|
||||||
|
result.stderr?.toString()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function tearDownTurnContainer(): void {
|
||||||
|
debug('tearDownTurnContainer');
|
||||||
|
child.spawnSync('docker', ['rm', '--force', '--volumes', 'coturn']);
|
||||||
|
}
|
|
@ -7,7 +7,7 @@ import type { App } from '../playwright';
|
||||||
import { Bootstrap } from '../bootstrap';
|
import { Bootstrap } from '../bootstrap';
|
||||||
import { typeIntoInput, waitForEnabledComposer } from '../helpers';
|
import { typeIntoInput, waitForEnabledComposer } from '../helpers';
|
||||||
|
|
||||||
describe('callMessages', function (this: Mocha.Suite) {
|
describe('twoClients', function twoClients(this: Mocha.Suite) {
|
||||||
this.timeout(durations.MINUTE);
|
this.timeout(durations.MINUTE);
|
||||||
|
|
||||||
let bootstrap1: Bootstrap;
|
let bootstrap1: Bootstrap;
|
||||||
|
@ -56,12 +56,12 @@ describe('callMessages', function (this: Mocha.Suite) {
|
||||||
app2 = await bootstrap2.link();
|
app2 = await bootstrap2.link();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async function (this: Mocha.Context) {
|
afterEach(async function after(this: Mocha.Context) {
|
||||||
if (!bootstrap1) {
|
if (!bootstrap1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await bootstrap2.maybeSaveLogs(this.currentTest, app2);
|
|
||||||
await bootstrap1.maybeSaveLogs(this.currentTest, app1);
|
await bootstrap1.maybeSaveLogs(this.currentTest, app1);
|
||||||
|
await bootstrap2.maybeSaveLogs(this.currentTest, app2);
|
||||||
|
|
||||||
await app2.close();
|
await app2.close();
|
||||||
await app1.close();
|
await app1.close();
|
|
@ -145,6 +145,10 @@ export class App extends EventEmitter {
|
||||||
return this.#waitForEvent('storageServiceComplete');
|
return this.#waitForEvent('storageServiceComplete');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async waitForWindow(): Promise<Page> {
|
||||||
|
return this.#app.waitForEvent('window');
|
||||||
|
}
|
||||||
|
|
||||||
public async waitForManifestVersion(version: number): Promise<void> {
|
public async waitForManifestVersion(version: number): Promise<void> {
|
||||||
// eslint-disable-next-line no-constant-condition
|
// eslint-disable-next-line no-constant-condition
|
||||||
while (true) {
|
while (true) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue