Skip to content
This repository was archived by the owner on Sep 11, 2024. It is now read-only.

Commit 69593d0

Browse files
authored
Merge branch 'develop' into AudioPlayer
2 parents e17e7da + 1436f23 commit 69593d0

File tree

83 files changed

+2891
-488
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

83 files changed

+2891
-488
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ module.exports = {
9292
files: [
9393
"src/**/*.{ts,tsx}",
9494
"test/**/*.{ts,tsx}",
95+
"cypress/**/*.ts",
9596
],
9697
extends: [
9798
"plugin:matrix-org/typescript",
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Produce a build of element-web with this version of react-sdk
2+
# and any matching branches of element-web and js-sdk, output it
3+
# as an artifact and run integration tests.
4+
name: Element Web - Build and Test
5+
on:
6+
pull_request:
7+
jobs:
8+
build:
9+
runs-on: ubuntu-latest
10+
env:
11+
# This must be set for fetchdep.sh to get the right branch
12+
PR_NUMBER: ${{github.event.number}}
13+
steps:
14+
- uses: actions/checkout@v2
15+
- name: Build
16+
run: scripts/ci/layered.sh && cd element-web && cp element.io/develop/config.json config.json && CI_PACKAGE=true yarn build
17+
- name: Upload Artifact
18+
uses: actions/upload-artifact@v2
19+
with:
20+
name: previewbuild
21+
path: element-web/webapp
22+
# We'll only use this in a triggered job, then we're done with it
23+
retention-days: 1
24+
cypress:
25+
needs: build
26+
runs-on: ubuntu-latest
27+
steps:
28+
- uses: actions/checkout@v2
29+
- name: Download build
30+
uses: actions/download-artifact@v3
31+
with:
32+
name: previewbuild
33+
path: webapp
34+
- name: Run Cypress tests
35+
uses: cypress-io/github-action@v2
36+
with:
37+
# The built in Electron runner seems to grind to a halt trying
38+
# to run the tests, so use chrome.
39+
browser: chrome
40+
start: npx serve -p 8080 webapp
41+
- name: Upload Artifact
42+
if: failure()
43+
uses: actions/upload-artifact@v2
44+
with:
45+
name: cypress-results
46+
path: |
47+
cypress/screenshots
48+
cypress/videos
49+
cypress/synapselogs

.github/workflows/layered-build.yaml

Lines changed: 0 additions & 23 deletions
This file was deleted.

.github/workflows/netlify.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
name: Upload Preview Build to Netlify
44
on:
55
workflow_run:
6-
workflows: ["Layered Preview Build"]
6+
workflows: ["Element Web - Build and Test"]
77
types:
88
- completed
99
jobs:

.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,11 @@ package-lock.json
1919

2020
.vscode
2121
.vscode/
22+
23+
/cypress/videos
24+
/cypress/downloads
25+
/cypress/screenshots
26+
/cypress/synapselogs
27+
# These could have files in them but don't currently
28+
# Cypress will still auto-create them though...
29+
/cypress/fixtures

cypress.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"baseUrl": "http://localhost:8080",
3+
"videoUploadOnPasses": false
4+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
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+
/// <reference types="cypress" />
18+
19+
import { SynapseInstance } from "../../plugins/synapsedocker/index";
20+
21+
describe("Registration", () => {
22+
let synapseId;
23+
let synapsePort;
24+
25+
beforeEach(() => {
26+
cy.task<SynapseInstance>("synapseStart", "consent").then(result => {
27+
synapseId = result.synapseId;
28+
synapsePort = result.port;
29+
});
30+
cy.visit("/#/register");
31+
});
32+
33+
afterEach(() => {
34+
cy.task("synapseStop", synapseId);
35+
});
36+
37+
it("registers an account and lands on the home screen", () => {
38+
cy.get(".mx_ServerPicker_change", { timeout: 15000 }).click();
39+
cy.get(".mx_ServerPickerDialog_otherHomeserver").type(`http://localhost:${synapsePort}`);
40+
cy.get(".mx_ServerPickerDialog_continue").click();
41+
// wait for the dialog to go away
42+
cy.get('.mx_ServerPickerDialog').should('not.exist');
43+
cy.get("#mx_RegistrationForm_username").type("alice");
44+
cy.get("#mx_RegistrationForm_password").type("totally a great password");
45+
cy.get("#mx_RegistrationForm_passwordConfirm").type("totally a great password");
46+
cy.get(".mx_Login_submit").click();
47+
cy.get(".mx_RegistrationEmailPromptDialog button.mx_Dialog_primary").click();
48+
cy.get(".mx_InteractiveAuthEntryComponents_termsPolicy input").click();
49+
cy.get(".mx_InteractiveAuthEntryComponents_termsSubmit").click();
50+
cy.url().should('contain', '/#/home');
51+
});
52+
});

cypress/plugins/index.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
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+
/// <reference types="cypress" />
18+
19+
import { synapseDocker } from "./synapsedocker/index";
20+
21+
export default function(on, config) {
22+
synapseDocker(on, config);
23+
}
Lines changed: 212 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,212 @@
1+
/*
2+
Copyright 2022 The Matrix.org Foundation C.I.C.
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+
/// <reference types="cypress" />
18+
19+
import * as path from "path";
20+
import * as os from "os";
21+
import * as crypto from "crypto";
22+
import * as childProcess from "child_process";
23+
import * as fse from "fs-extra";
24+
25+
// A cypress plugins to add command to start & stop synapses in
26+
// docker with preset templates.
27+
28+
interface SynapseConfig {
29+
configDir: string;
30+
registrationSecret: string;
31+
}
32+
33+
export interface SynapseInstance extends SynapseConfig {
34+
synapseId: string;
35+
port: number;
36+
}
37+
38+
const synapses = new Map<string, SynapseInstance>();
39+
40+
function randB64Bytes(numBytes: number): string {
41+
return crypto.randomBytes(numBytes).toString("base64").replace(/=*$/, "");
42+
}
43+
44+
async function cfgDirFromTemplate(template: string): Promise<SynapseConfig> {
45+
const templateDir = path.join(__dirname, "templates", template);
46+
47+
const stats = await fse.stat(templateDir);
48+
if (!stats?.isDirectory) {
49+
throw new Error(`No such template: ${template}`);
50+
}
51+
const tempDir = await fse.mkdtemp(path.join(os.tmpdir(), 'react-sdk-synapsedocker-'));
52+
53+
// change permissions on the temp directory so the docker container can see its contents
54+
await fse.chmod(tempDir, 0o777);
55+
56+
// copy the contents of the template dir, omitting homeserver.yaml as we'll template that
57+
console.log(`Copy ${templateDir} -> ${tempDir}`);
58+
await fse.copy(templateDir, tempDir, { filter: f => path.basename(f) !== 'homeserver.yaml' });
59+
60+
const registrationSecret = randB64Bytes(16);
61+
const macaroonSecret = randB64Bytes(16);
62+
const formSecret = randB64Bytes(16);
63+
64+
// now copy homeserver.yaml, applying sustitutions
65+
console.log(`Gen ${path.join(templateDir, "homeserver.yaml")}`);
66+
let hsYaml = await fse.readFile(path.join(templateDir, "homeserver.yaml"), "utf8");
67+
hsYaml = hsYaml.replace(/{{REGISTRATION_SECRET}}/g, registrationSecret);
68+
hsYaml = hsYaml.replace(/{{MACAROON_SECRET_KEY}}/g, macaroonSecret);
69+
hsYaml = hsYaml.replace(/{{FORM_SECRET}}/g, formSecret);
70+
await fse.writeFile(path.join(tempDir, "homeserver.yaml"), hsYaml);
71+
72+
// now generate a signing key (we could use synapse's config generation for
73+
// this, or we could just do this...)
74+
// NB. This assumes the homeserver.yaml specifies the key in this location
75+
const signingKey = randB64Bytes(32);
76+
console.log(`Gen ${path.join(templateDir, "localhost.signing.key")}`);
77+
await fse.writeFile(path.join(tempDir, "localhost.signing.key"), `ed25519 x ${signingKey}`);
78+
79+
return {
80+
configDir: tempDir,
81+
registrationSecret,
82+
};
83+
}
84+
85+
// Start a synapse instance: the template must be the name of
86+
// one of the templates in the cypress/plugins/synapsedocker/templates
87+
// directory
88+
async function synapseStart(template: string): Promise<SynapseInstance> {
89+
const synCfg = await cfgDirFromTemplate(template);
90+
91+
console.log(`Starting synapse with config dir ${synCfg.configDir}...`);
92+
93+
const containerName = `react-sdk-cypress-synapse-${crypto.randomBytes(4).toString("hex")}`;
94+
95+
const synapseId = await new Promise<string>((resolve, reject) => {
96+
childProcess.execFile('docker', [
97+
"run",
98+
"--name", containerName,
99+
"-d",
100+
"-v", `${synCfg.configDir}:/data`,
101+
"-p", "8008/tcp",
102+
"matrixdotorg/synapse:develop",
103+
"run",
104+
], (err, stdout) => {
105+
if (err) reject(err);
106+
resolve(stdout.trim());
107+
});
108+
});
109+
110+
// Get the port that docker allocated: specifying only one
111+
// port above leaves docker to just grab a free one, although
112+
// in hindsight we need to put the port in public_baseurl in the
113+
// config really, so this will probably need changing to use a fixed
114+
// / configured port.
115+
const port = await new Promise<number>((resolve, reject) => {
116+
childProcess.execFile('docker', [
117+
"port", synapseId, "8008",
118+
], { encoding: 'utf8' }, (err, stdout) => {
119+
if (err) reject(err);
120+
resolve(Number(stdout.trim().split(":")[1]));
121+
});
122+
});
123+
124+
synapses.set(synapseId, Object.assign({
125+
port,
126+
synapseId,
127+
}, synCfg));
128+
129+
console.log(`Started synapse with id ${synapseId} on port ${port}.`);
130+
return synapses.get(synapseId);
131+
}
132+
133+
async function synapseStop(id) {
134+
const synCfg = synapses.get(id);
135+
136+
if (!synCfg) throw new Error("Unknown synapse ID");
137+
138+
try {
139+
const synapseLogsPath = path.join("cypress", "synapselogs", id);
140+
await fse.ensureDir(synapseLogsPath);
141+
142+
const stdoutFile = await fse.open(path.join(synapseLogsPath, "stdout.log"), "w");
143+
const stderrFile = await fse.open(path.join(synapseLogsPath, "stderr.log"), "w");
144+
await new Promise<void>((resolve, reject) => {
145+
childProcess.spawn('docker', [
146+
"logs",
147+
id,
148+
], {
149+
stdio: ["ignore", stdoutFile, stderrFile],
150+
}).once('close', resolve);
151+
});
152+
await fse.close(stdoutFile);
153+
await fse.close(stderrFile);
154+
155+
await new Promise<void>((resolve, reject) => {
156+
childProcess.execFile('docker', [
157+
"stop",
158+
id,
159+
], err => {
160+
if (err) reject(err);
161+
resolve();
162+
});
163+
});
164+
} finally {
165+
await new Promise<void>((resolve, reject) => {
166+
childProcess.execFile('docker', [
167+
"rm",
168+
id,
169+
], err => {
170+
if (err) reject(err);
171+
resolve();
172+
});
173+
});
174+
}
175+
176+
await fse.remove(synCfg.configDir);
177+
178+
synapses.delete(id);
179+
180+
console.log(`Stopped synapse id ${id}.`);
181+
// cypres deliberately fails if you return 'undefined', so
182+
// return null to signal all is well and we've handled the task.
183+
return null;
184+
}
185+
186+
/**
187+
* @type {Cypress.PluginConfig}
188+
*/
189+
// eslint-disable-next-line no-unused-vars
190+
export function synapseDocker(on, config) {
191+
on("task", {
192+
synapseStart, synapseStop,
193+
});
194+
195+
on("after:spec", async (spec) => {
196+
// Cleans up any remaining synapse instances after a spec run
197+
// This is on the theory that we should avoid re-using synapse
198+
// instances between spec runs: they should be cheap enough to
199+
// start that we can have a separate one for each spec run or even
200+
// test. If we accidentally re-use synapses, we could inadvertantly
201+
// make our tests depend on each other.
202+
for (const synId of synapses.keys()) {
203+
console.warn(`Cleaning up synapse ID ${synId} after ${spec.name}`);
204+
await synapseStop(synId);
205+
}
206+
});
207+
208+
on("before:run", async () => {
209+
// tidy up old synapse log files before each run
210+
await fse.emptyDir(path.join("cypress", "synapselogs"));
211+
});
212+
}

0 commit comments

Comments
 (0)