Improving tests, WIP

This commit is contained in:
David Négrier 2021-12-03 09:28:30 +01:00
parent 9bba6069b4
commit 78ee4009c8
11 changed files with 276 additions and 25 deletions

View File

@ -59,9 +59,43 @@ $ docker-compose exec back yarn run pretty
WorkAdventure is based on a video game engine (Phaser), and video games are not the easiest programs to unit test. WorkAdventure is based on a video game engine (Phaser), and video games are not the easiest programs to unit test.
Nevertheless, if your code can be unit tested, please provide a unit test (we use Jasmine). Nevertheless, if your code can be unit tested, please provide a unit test (we use Jasmine), or an end-to-end test (we use Testcafe).
If you are providing a new feature, you should setup a test map in the `maps/tests` directory. The test map should contain If you are providing a new feature, you should setup a test map in the `maps/tests` directory. The test map should contain
some description text describing how to test the feature. Finally, you should modify the `maps/tests/index.html` file some description text describing how to test the feature.
to add a reference to your newly created test map.
* if the features is meant to be manually tested, you should modify the `maps/tests/index.html` file to add a reference
to your newly created test map
* if the features can be automatically tested, please provide a testcafe test
#### Running testcafe tests
End-to-end tests are available in the "/tests" directory.
To run these tests locally:
```console
$ LIVE_RELOAD=0 docker-compose up -d
$ cd tests
$ npm install
$ npm run test
```
Note: If your tests fail on a Javascript error in "sockjs", this is due to the
Webpack live reload. The Webpack live reload feature is conflicting with testcafe. This is why we recommend starting
WorkAdventure with the `LIVE_RELOAD=0` environment variable.
End-to-end tests can take a while to run. To run only one test, use:
```console
$ npm run test -- tests/[name of the test file].ts
```
You can also run the tests inside a container (but you will not have visual feedbacks on your test, so we recommend using
the local tests).
```console
$ LIVE_RELOAD=0 docker-compose up -d
# Wait 2-3 minutes for the environment to start, then:
$ docker-compose -f docker-compose.testcafe.yaml up
```

View File

@ -3,6 +3,7 @@ import { Scene } from "phaser";
import { ErrorScene, ErrorSceneName } from "../Reconnecting/ErrorScene"; import { ErrorScene, ErrorSceneName } from "../Reconnecting/ErrorScene";
import { WAError } from "../Reconnecting/WAError"; import { WAError } from "../Reconnecting/WAError";
import { waScaleManager } from "../Services/WaScaleManager"; import { waScaleManager } from "../Services/WaScaleManager";
import { ReconnectingTextures } from "../Reconnecting/ReconnectingScene";
export const EntrySceneName = "EntryScene"; export const EntrySceneName = "EntryScene";
@ -17,6 +18,14 @@ export class EntryScene extends Scene {
}); });
} }
// From the very start, let's preload images used in the ReconnectingScene.
preload() {
this.load.image(ReconnectingTextures.icon, "static/images/favicons/favicon-32x32.png");
// Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
this.load.bitmapFont(ReconnectingTextures.mainFont, "resources/fonts/arcade.png", "resources/fonts/arcade.xml");
this.load.spritesheet("cat", "resources/characters/pipoya/Cat 01-1.png", { frameWidth: 32, frameHeight: 32 });
}
create() { create() {
gameManager gameManager
.init(this.scene) .init(this.scene)

View File

@ -3,7 +3,7 @@ import Image = Phaser.GameObjects.Image;
import Sprite = Phaser.GameObjects.Sprite; import Sprite = Phaser.GameObjects.Sprite;
export const ReconnectingSceneName = "ReconnectingScene"; export const ReconnectingSceneName = "ReconnectingScene";
enum ReconnectingTextures { export enum ReconnectingTextures {
icon = "icon", icon = "icon",
mainFont = "main_font", mainFont = "main_font",
} }

View File

@ -3,7 +3,6 @@ const BROWSER = process.env.BROWSER || "chrome --use-fake-ui-for-media-stream --
module.exports = { module.exports = {
"browsers": BROWSER, "browsers": BROWSER,
"hostname": "localhost", "hostname": "localhost",
//"skipJsErrors": true,
"src": "tests/", "src": "tests/",
"screenshots": { "screenshots": {
"path": "screenshots/", "path": "screenshots/",
@ -12,4 +11,15 @@ module.exports = {
}, },
"assertionTimeout": 10000, "assertionTimeout": 10000,
"selectorTimeout": 40000, "selectorTimeout": 40000,
/*"skipJsErrors": true,
"clientScripts": [ { "content": `
window.addEventListener('error', function (e) {
if (e instanceof Error && e.message.includes('_jp')) {
console.log('Ignoring sockjs related error');
return;
}
throw e;
});
` } ]*/
} }

View File

@ -5,7 +5,8 @@
"packages": { "packages": {
"": { "": {
"dependencies": { "dependencies": {
"@types/dockerode": "^3.3.0" "@types/dockerode": "^3.3.0",
"axios": "^0.24.0"
}, },
"devDependencies": { "devDependencies": {
"dockerode": "^3.3.1", "dockerode": "^3.3.1",
@ -2095,6 +2096,14 @@
"node": ">= 4.5.0" "node": ">= 4.5.0"
} }
}, },
"node_modules/axios": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
"dependencies": {
"follow-redirects": "^1.14.4"
}
},
"node_modules/babel-plugin-dynamic-import-node": { "node_modules/babel-plugin-dynamic-import-node": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz",
@ -3031,6 +3040,25 @@
"node": ">=6" "node": ">=6"
} }
}, },
"node_modules/follow-redirects": {
"version": "1.14.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz",
"integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/fs-constants": { "node_modules/fs-constants": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
@ -6843,6 +6871,14 @@
"integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
"dev": true "dev": true
}, },
"axios": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz",
"integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==",
"requires": {
"follow-redirects": "^1.14.4"
}
},
"babel-plugin-dynamic-import-node": { "babel-plugin-dynamic-import-node": {
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz",
@ -7594,6 +7630,11 @@
"locate-path": "^3.0.0" "locate-path": "^3.0.0"
} }
}, },
"follow-redirects": {
"version": "1.14.5",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.5.tgz",
"integrity": "sha512-wtphSXy7d4/OR+MvIFbCVBDzZ5520qV8XfPklSN5QtxuMUJZ+b0Wnst1e1lCDocfzuCkHqj8k0FpZqO+UIaKNA=="
},
"fs-constants": { "fs-constants": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",

View File

@ -7,6 +7,7 @@
"test": "testcafe" "test": "testcafe"
}, },
"dependencies": { "dependencies": {
"@types/dockerode": "^3.3.0" "@types/dockerode": "^3.3.0",
"axios": "^0.24.0"
} }
} }

View File

@ -4,7 +4,7 @@ const fs = require('fs');
const Docker = require('dockerode'); const Docker = require('dockerode');
import { Selector } from 'testcafe'; import { Selector } from 'testcafe';
import {userAlice} from "./utils/roles"; import {userAlice} from "./utils/roles";
import {findContainer, rebootBack, rebootPusher, rebootRedis, startContainer, stopContainer} from "./utils/containers"; import {findContainer, rebootBack, rebootPusher, resetRedis, startContainer, stopContainer} from "./utils/containers";
fixture `Reconnection` fixture `Reconnection`
.page `http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/mousewheel.json`; .page `http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/mousewheel.json`;

View File

@ -1,7 +1,9 @@
//import Docker from "dockerode"; //import Docker from "dockerode";
//import * as Dockerode from "dockerode"; //import * as Dockerode from "dockerode";
import Dockerode = require( 'dockerode') import Dockerode = require( 'dockerode')
const util = require('util');
const exec = util.promisify(require('child_process').exec);
const { execSync } = require('child_process');
/** /**
* Returns a container ID based on the container name. * Returns a container ID based on the container name.
@ -33,19 +35,81 @@ export async function startContainer(container: Dockerode.ContainerInfo): Promis
} }
export async function rebootBack(): Promise<void> { export async function rebootBack(): Promise<void> {
const container = await findContainer('back'); let stdout = execSync('docker-compose up --force-recreate -d back', {
cwd: __dirname + '/../../../'
});
/*const container = await findContainer('back');
await stopContainer(container); await stopContainer(container);
await startContainer(container); await startContainer(container);*/
}
export function rebootTraefik(): void {
let stdout = execSync('docker-compose up --force-recreate -d reverse-proxy', {
cwd: __dirname + '/../../../'
});
//console.log('rebootTraefik', stdout);
} }
export async function rebootPusher(): Promise<void> { export async function rebootPusher(): Promise<void> {
const container = await findContainer('pusher'); let stdout = execSync('docker-compose up --force-recreate -d pusher', {
cwd: __dirname + '/../../../'
});
/*const container = await findContainer('pusher');
await stopContainer(container); await stopContainer(container);
await startContainer(container); await startContainer(container);*/
} }
export async function rebootRedis(): Promise<void> { export async function resetRedis(): Promise<void> {
const container = await findContainer('redis'); let stdout = execSync('docker-compose stop redis', {
await stopContainer(container); cwd: __dirname + '/../../../'
await startContainer(container); });
//console.log('rebootRedis', stdout);
stdout = execSync('docker-compose rm -f redis', {
cwd: __dirname + '/../../../'
});
//console.log('rebootRedis', stdout);
stdout = execSync('docker-compose up --force-recreate -d redis', {
cwd: __dirname + '/../../../'
});
//console.log('rebootRedis', stdout);
/*
let stdout = execSync('docker-compose stop redis', {
cwd: __dirname + '/../../../'
});
console.log('stdout:', stdout);
stdout = execSync('docker-compose rm redis', {
cwd: __dirname + '/../../../'
});
//const { stdout, stderr } = await exec('docker-compose down redis');
console.log('stdout:', stdout);
//console.log('stderr:', stderr);
const { stdout2, stderr2 } = await exec('docker-compose up -d redis');
console.log('stdout:', stdout2);
console.log('stderr:', stderr2);
*/
/*const container = await findContainer('redis');
//await stopContainer(container);
//await startContainer(container);
const docker = new Dockerode();
await docker.getContainer(container.Id).stop();
await docker.getContainer(container.Id).remove();
const newContainer = await docker.createContainer(container);
await newContainer.start();*/
}
export function stopRedis(): void {
let stdout = execSync('docker-compose stop redis', {
cwd: __dirname + '/../../../'
});
}
export function startRedis(): void {
let stdout = execSync('docker-compose start redis', {
cwd: __dirname + '/../../../'
});
} }

View File

@ -0,0 +1,17 @@
import axios from "axios";
export async function getPusherDump(): Promise<any> {
return (await axios({
url: 'http://pusher.workadventure.localhost/dump?token=123',
method: 'get',
})).data;
}
export async function getBackDump(): Promise<any> {
return (await axios({
url: 'http://api.workadventure.localhost/dump?token=123',
method: 'get',
})).data;
}

View File

@ -1,5 +1,16 @@
import { Role } from 'testcafe'; import { Role } from 'testcafe';
export const userAliceOnPage = (url: string) => Role(url, async t => {
await t
.typeText('input[name="loginSceneName"]', 'Alice')
.click('button.loginSceneFormSubmit')
.click('button.selectCharacterButtonRight')
.click('button.selectCharacterButtonRight')
.click('button.selectCharacterSceneFormSubmit')
.click('button.letsgo');
});
export const userAlice = Role('http://play.workadventure.localhost/', async t => { export const userAlice = Role('http://play.workadventure.localhost/', async t => {
await t await t
.typeText('input[name="loginSceneName"]', 'Alice') .typeText('input[name="loginSceneName"]', 'Alice')

View File

@ -3,8 +3,17 @@ import {assertLogMessage} from "./utils/log";
const fs = require('fs'); const fs = require('fs');
const Docker = require('dockerode'); const Docker = require('dockerode');
import { Selector } from 'testcafe'; import { Selector } from 'testcafe';
import {userAlice} from "./utils/roles"; import {userAlice, userAliceOnPage} from "./utils/roles";
import {findContainer, rebootBack, rebootPusher, rebootRedis, startContainer, stopContainer} from "./utils/containers"; import {
findContainer,
rebootBack,
rebootPusher,
resetRedis,
rebootTraefik,
startContainer,
stopContainer, stopRedis, startRedis
} from "./utils/containers";
import {getPusherDump} from "./utils/debug";
// Note: we are also testing that passing a random query parameter does not cause any issue. // Note: we are also testing that passing a random query parameter does not cause any issue.
fixture `Variables` fixture `Variables`
@ -14,21 +23,75 @@ test("Test that variables storage works", async (t: TestController) => {
const variableInput = Selector('#textField'); const variableInput = Selector('#textField');
await resetRedis();
await Promise.all([ await Promise.all([
rebootBack(), rebootBack(),
rebootRedis(),
rebootPusher(), rebootPusher(),
]); ]);
await t.useRole(userAlice) //const mainWindow = await t.getCurrentWindow();
await t.useRole(userAliceOnPage('http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/shared_variables.json?somerandomparam=1'))
.switchToIframe("#cowebsite-buffer iframe") .switchToIframe("#cowebsite-buffer iframe")
.expect(variableInput.value).eql('default value') .expect(variableInput.value).eql('default value')
.selectText(variableInput)
.pressKey('delete')
.typeText(variableInput, 'new value') .typeText(variableInput, 'new value')
.switchToPreviousWindow() .pressKey('tab')
.switchToMainWindow()
//.switchToWindow(mainWindow)
.wait(500)
// reload // reload
/*.navigateTo('http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/shared_variables.json') .navigateTo('http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/shared_variables.json')
.switchToIframe("#cowebsite-buffer iframe") .switchToIframe("#cowebsite-buffer iframe")
.expect(variableInput.value).eql('new value')*/ .expect(variableInput.value).eql('new value')
//.debug()
.switchToMainWindow()
//.switchToWindow(mainWindow)
/*
.wait(5000)
//.debug()
.switchToIframe("#cowebsite-buffer iframe")
.expect(variableInput.value).eql('new value')
.switchToMainWindow();*/
// Now, let's kill the reverse proxy to cut the connexion
//console.log('Rebooting traefik');
await rebootTraefik();
//console.log('Rebooting done');
await t
.switchToIframe("#cowebsite-buffer iframe")
.expect(variableInput.value).eql('new value')
stopRedis();
// Redis is stopped, let's try to modify a variable.
await t.selectText(variableInput)
.pressKey('delete')
.typeText(variableInput, 'value set while Redis stopped')
.pressKey('tab')
.switchToMainWindow()
startRedis();
// Navigate to some other map so that the pusher connection is freed.
await t.navigateTo('http://maps.workadventure.localhost/tests/');
// TODO: check that Back and Pusher rooms are unloaded
// TODO: check that Back and Pusher rooms are unloaded
// TODO: check that Back and Pusher rooms are unloaded
// TODO: check that Back and Pusher rooms are unloaded
console.log(await getPusherDump());
await t.navigateTo('http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/shared_variables.json')
.switchToIframe("#cowebsite-buffer iframe")
// Redis will reconnect automatically and will store the variable on reconnect!
// So we should see the new value.
.expect(variableInput.value).eql('value set while Redis stopped')
.switchToMainWindow()
@ -50,7 +113,7 @@ test("Test that variables storage works", async (t: TestController) => {
} }
}); });
/*
test("Test that variables cache in the back don't prevent setting a variable in case the map changes", async (t: TestController) => { test("Test that variables cache in the back don't prevent setting a variable in case the map changes", async (t: TestController) => {
// Let's start by visiting a map that DOES not have the variable. // Let's start by visiting a map that DOES not have the variable.
fs.copyFileSync('../maps/tests/Variables/Cache/variables_cache_1.json', '../maps/tests/Variables/Cache/variables_tmp.json'); fs.copyFileSync('../maps/tests/Variables/Cache/variables_cache_1.json', '../maps/tests/Variables/Cache/variables_tmp.json');
@ -77,3 +140,4 @@ test("Test that variables cache in the back don't prevent setting a variable in
} }
}); });
*/