diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8bbbc93e..2cf9435e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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. -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 -some description text describing how to test the feature. Finally, you should modify the `maps/tests/index.html` file -to add a reference to your newly created test map. +some description text describing how to test the feature. +* 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 +``` diff --git a/front/src/Phaser/Login/EntryScene.ts b/front/src/Phaser/Login/EntryScene.ts index 63181ae9..3fb2e6b5 100644 --- a/front/src/Phaser/Login/EntryScene.ts +++ b/front/src/Phaser/Login/EntryScene.ts @@ -3,6 +3,7 @@ import { Scene } from "phaser"; import { ErrorScene, ErrorSceneName } from "../Reconnecting/ErrorScene"; import { WAError } from "../Reconnecting/WAError"; import { waScaleManager } from "../Services/WaScaleManager"; +import { ReconnectingTextures } from "../Reconnecting/ReconnectingScene"; 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() { gameManager .init(this.scene) diff --git a/front/src/Phaser/Reconnecting/ReconnectingScene.ts b/front/src/Phaser/Reconnecting/ReconnectingScene.ts index 3c8a966c..f02ca75a 100644 --- a/front/src/Phaser/Reconnecting/ReconnectingScene.ts +++ b/front/src/Phaser/Reconnecting/ReconnectingScene.ts @@ -3,7 +3,7 @@ import Image = Phaser.GameObjects.Image; import Sprite = Phaser.GameObjects.Sprite; export const ReconnectingSceneName = "ReconnectingScene"; -enum ReconnectingTextures { +export enum ReconnectingTextures { icon = "icon", mainFont = "main_font", } diff --git a/tests/.testcaferc.js b/tests/.testcaferc.js index 9f7628c5..8011038c 100644 --- a/tests/.testcaferc.js +++ b/tests/.testcaferc.js @@ -3,7 +3,6 @@ const BROWSER = process.env.BROWSER || "chrome --use-fake-ui-for-media-stream -- module.exports = { "browsers": BROWSER, "hostname": "localhost", - //"skipJsErrors": true, "src": "tests/", "screenshots": { "path": "screenshots/", @@ -12,4 +11,15 @@ module.exports = { }, "assertionTimeout": 10000, "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; + }); + ` } ]*/ } diff --git a/tests/package-lock.json b/tests/package-lock.json index fc3cf124..2fb66e00 100644 --- a/tests/package-lock.json +++ b/tests/package-lock.json @@ -5,7 +5,8 @@ "packages": { "": { "dependencies": { - "@types/dockerode": "^3.3.0" + "@types/dockerode": "^3.3.0", + "axios": "^0.24.0" }, "devDependencies": { "dockerode": "^3.3.1", @@ -2095,6 +2096,14 @@ "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": { "version": "2.3.3", "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_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": { "version": "1.0.0", "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==", "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": { "version": "2.3.3", "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" } }, + "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": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", diff --git a/tests/package.json b/tests/package.json index f36c5ea4..a21a3881 100644 --- a/tests/package.json +++ b/tests/package.json @@ -7,6 +7,7 @@ "test": "testcafe" }, "dependencies": { - "@types/dockerode": "^3.3.0" + "@types/dockerode": "^3.3.0", + "axios": "^0.24.0" } } diff --git a/tests/tests/reconnect.ts b/tests/tests/reconnect.ts index b4a052c5..fa7352da 100644 --- a/tests/tests/reconnect.ts +++ b/tests/tests/reconnect.ts @@ -4,7 +4,7 @@ const fs = require('fs'); const Docker = require('dockerode'); import { Selector } from 'testcafe'; 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` .page `http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/mousewheel.json`; diff --git a/tests/tests/utils/containers.ts b/tests/tests/utils/containers.ts index 6767b6dc..454340ff 100644 --- a/tests/tests/utils/containers.ts +++ b/tests/tests/utils/containers.ts @@ -1,7 +1,9 @@ //import Docker from "dockerode"; //import * as Dockerode from "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. @@ -33,19 +35,81 @@ export async function startContainer(container: Dockerode.ContainerInfo): Promis } export async function rebootBack(): Promise { - 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 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 { - 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 startContainer(container); + await startContainer(container);*/ } -export async function rebootRedis(): Promise { - const container = await findContainer('redis'); - await stopContainer(container); - await startContainer(container); +export async function resetRedis(): Promise { + let stdout = execSync('docker-compose stop redis', { + cwd: __dirname + '/../../../' + }); + //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 + '/../../../' + }); } diff --git a/tests/tests/utils/debug.ts b/tests/tests/utils/debug.ts new file mode 100644 index 00000000..c29ac5b0 --- /dev/null +++ b/tests/tests/utils/debug.ts @@ -0,0 +1,17 @@ +import axios from "axios"; + +export async function getPusherDump(): Promise { + return (await axios({ + url: 'http://pusher.workadventure.localhost/dump?token=123', + method: 'get', + })).data; +} + + +export async function getBackDump(): Promise { + return (await axios({ + url: 'http://api.workadventure.localhost/dump?token=123', + method: 'get', + })).data; +} + diff --git a/tests/tests/utils/roles.ts b/tests/tests/utils/roles.ts index 7ed4cad7..5282e564 100644 --- a/tests/tests/utils/roles.ts +++ b/tests/tests/utils/roles.ts @@ -1,5 +1,16 @@ 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 => { await t .typeText('input[name="loginSceneName"]', 'Alice') diff --git a/tests/tests/variables.ts b/tests/tests/variables.ts index a3d16fda..1e3a516d 100644 --- a/tests/tests/variables.ts +++ b/tests/tests/variables.ts @@ -3,8 +3,17 @@ import {assertLogMessage} from "./utils/log"; const fs = require('fs'); const Docker = require('dockerode'); import { Selector } from 'testcafe'; -import {userAlice} from "./utils/roles"; -import {findContainer, rebootBack, rebootPusher, rebootRedis, startContainer, stopContainer} from "./utils/containers"; +import {userAlice, userAliceOnPage} from "./utils/roles"; +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. fixture `Variables` @@ -14,21 +23,75 @@ test("Test that variables storage works", async (t: TestController) => { const variableInput = Selector('#textField'); + await resetRedis(); + await Promise.all([ rebootBack(), - rebootRedis(), 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") .expect(variableInput.value).eql('default value') + .selectText(variableInput) + .pressKey('delete') .typeText(variableInput, 'new value') - .switchToPreviousWindow() + .pressKey('tab') + .switchToMainWindow() + //.switchToWindow(mainWindow) + .wait(500) // 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") - .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) => { // 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'); @@ -77,3 +140,4 @@ test("Test that variables cache in the back don't prevent setting a variable in } }); +*/