merge latest dev

This commit is contained in:
_Bastler 2021-11-24 13:00:27 +01:00
parent 1873ac1836
commit 3b98888c3a
26 changed files with 9451 additions and 64 deletions

51
.github/workflows/end_to_end_tests.yml vendored Normal file
View File

@ -0,0 +1,51 @@
# https://help.github.com/en/categories/automating-your-workflow-with-github-actions
name: "End to end tests"
on:
push:
branches:
- master
- develop
pull_request:
jobs:
end-to-end-tests:
name: "End-to-end testcafe tests"
runs-on: "ubuntu-latest"
steps:
- name: "Checkout"
uses: "actions/checkout@v2.0.0"
- name: "Setup .env file"
run: cp .env.template .env
- name: "Edit ownership of file for test cases"
run: sudo chown 1000:1000 -R .
- name: "Start environment"
run: docker-compose up -d
- name: "Wait for environment to build (and downloading testcafe image)"
run: (docker-compose -f docker-compose.testcafe.yml pull &) && docker-compose logs -f --tail=0 front | grep -m 1 "Compiled successfully"
- name: "Run tests"
run: docker-compose -f docker-compose.testcafe.yml up --exit-code-from testcafe
- name: Upload failed tests
if: ${{ failure() }}
uses: actions/upload-artifact@v2
with:
name: my-artifact
path: './tests/screenshots/'
- name: Display state
if: ${{ failure() }}
run: docker-compose ps
- name: Display logs
if: ${{ failure() }}
run: docker-compose logs

View File

@ -14,7 +14,7 @@ export const MAX_PER_GROUP = parseInt(process.env.MAX_PER_GROUP || "4");
export const REDIS_HOST = process.env.REDIS_HOST || undefined; export const REDIS_HOST = process.env.REDIS_HOST || undefined;
export const REDIS_PORT = parseInt(process.env.REDIS_PORT || "6379") || 6379; export const REDIS_PORT = parseInt(process.env.REDIS_PORT || "6379") || 6379;
export const REDIS_PASSWORD = process.env.REDIS_PASSWORD || undefined; export const REDIS_PASSWORD = process.env.REDIS_PASSWORD || undefined;
export const DEBUG_BACK_IGNORE_LOCAL = process.env.DEBUG_BACK_IGNORE_LOCAL ? process.env.DEBUG_BACK_IGNORE_LOCAL == "true" : false; export const STORE_VARIABLES_FOR_LOCAL_MAPS = process.env.STORE_VARIABLES_FOR_LOCAL_MAPS === "true";
export { export {
MINIMUM_DISTANCE, MINIMUM_DISTANCE,

View File

@ -26,6 +26,7 @@ import { VariablesManager } from "../Services/VariablesManager";
import { ADMIN_API_URL } from "../Enum/EnvironmentVariable"; import { ADMIN_API_URL } from "../Enum/EnvironmentVariable";
import { LocalUrlError } from "../Services/LocalUrlError"; import { LocalUrlError } from "../Services/LocalUrlError";
import { emitErrorOnRoomSocket } from "../Services/MessageHelpers"; import { emitErrorOnRoomSocket } from "../Services/MessageHelpers";
import { VariableError } from "../Services/VariableError";
export type ConnectCallback = (user: User, group: Group) => void; export type ConnectCallback = (user: User, group: Group) => void;
export type DisconnectCallback = (user: User, group: Group) => void; export type DisconnectCallback = (user: User, group: Group) => void;
@ -336,6 +337,7 @@ export class GameRoom {
// First, let's check if "user" is allowed to modify the variable. // First, let's check if "user" is allowed to modify the variable.
const variableManager = await this.getVariableManager(); const variableManager = await this.getVariableManager();
try {
const readableBy = variableManager.setVariable(name, value, user); const readableBy = variableManager.setVariable(name, value, user);
// If the variable was not changed, let's not dispatch anything. // If the variable was not changed, let's not dispatch anything.
@ -361,6 +363,37 @@ export class GameRoom {
for (const socket of this.roomListeners) { for (const socket of this.roomListeners) {
socket.write(batchMessage); socket.write(batchMessage);
} }
} catch (e) {
if (e instanceof VariableError) {
// Ok, we have an error setting a variable. Either the user is trying to hack the map... or the map
// is not up to date. So let's try to reload the map from scratch.
if (this.variableManagerLastLoad === undefined) {
throw e;
}
const lastLoaded = new Date().getTime() - this.variableManagerLastLoad.getTime();
if (lastLoaded < 10000) {
console.log(
'An error occurred while setting the "' +
name +
"\" variable. But we tried to reload the map less than 10 seconds ago, so let's fail."
);
// Do not try to reload if we tried to reload less than 10 seconds ago.
throw e;
}
// Reset the variable manager
this.variableManagerPromise = undefined;
this.mapPromise = undefined;
console.log(
'An error occurred while setting the "' + name + "\" variable. Let's reload the map and try again"
);
// Try to set the variable again!
await this.setVariable(name, value, user);
} else {
throw e;
}
}
} }
public addZoneListener(call: ZoneSocket, x: number, y: number): Set<Movable> { public addZoneListener(call: ZoneSocket, x: number, y: number): Set<Movable> {
@ -449,9 +482,11 @@ export class GameRoom {
} }
private variableManagerPromise: Promise<VariablesManager> | undefined; private variableManagerPromise: Promise<VariablesManager> | undefined;
private variableManagerLastLoad: Date | undefined;
private getVariableManager(): Promise<VariablesManager> { private getVariableManager(): Promise<VariablesManager> {
if (!this.variableManagerPromise) { if (!this.variableManagerPromise) {
this.variableManagerLastLoad = new Date();
this.variableManagerPromise = this.getMap() this.variableManagerPromise = this.getMap()
.then((map) => { .then((map) => {
const variablesManager = new VariablesManager(this.roomUrl, map); const variablesManager = new VariablesManager(this.roomUrl, map);

View File

@ -5,13 +5,13 @@ import { promisify } from "util";
import { LocalUrlError } from "./LocalUrlError"; import { LocalUrlError } from "./LocalUrlError";
import { ITiledMap } from "@workadventure/tiled-map-type-guard"; import { ITiledMap } from "@workadventure/tiled-map-type-guard";
import { isTiledMap } from "@workadventure/tiled-map-type-guard/dist"; import { isTiledMap } from "@workadventure/tiled-map-type-guard/dist";
import { DEBUG_BACK_IGNORE_LOCAL } from "../Enum/EnvironmentVariable"; import { STORE_VARIABLES_FOR_LOCAL_MAPS } from "../Enum/EnvironmentVariable";
class MapFetcher { class MapFetcher {
async fetchMap(mapUrl: string): Promise<ITiledMap> { async fetchMap(mapUrl: string): Promise<ITiledMap> {
// Before trying to make the query, let's verify the map is actually on the open internet (and not a local test map) // Before trying to make the query, let's verify the map is actually on the open internet (and not a local test map)
if (!DEBUG_BACK_IGNORE_LOCAL && await this.isLocalUrl(mapUrl)) { if ((await this.isLocalUrl(mapUrl)) && !STORE_VARIABLES_FOR_LOCAL_MAPS) {
throw new LocalUrlError('URL for map "' + mapUrl + '" targets a local map'); throw new LocalUrlError('URL for map "' + mapUrl + '" targets a local map');
} }

View File

@ -0,0 +1,9 @@
/**
* Errors related to variable handling.
*/
export class VariableError extends Error {
constructor(message: string) {
super(message);
Object.setPrototypeOf(this, VariableError.prototype);
}
}

View File

@ -10,6 +10,7 @@ import {
import { User } from "_Model/User"; import { User } from "_Model/User";
import { variablesRepository } from "./Repository/VariablesRepository"; import { variablesRepository } from "./Repository/VariablesRepository";
import { redisClient } from "./RedisClient"; import { redisClient } from "./RedisClient";
import { VariableError } from "./VariableError";
interface Variable { interface Variable {
defaultValue?: string; defaultValue?: string;
@ -174,11 +175,13 @@ export class VariablesManager {
if (this.variableObjects) { if (this.variableObjects) {
variableObject = this.variableObjects.get(name); variableObject = this.variableObjects.get(name);
if (variableObject === undefined) { if (variableObject === undefined) {
throw new Error('Trying to set a variable "' + name + '" that is not defined as an object in the map.'); throw new VariableError(
'Trying to set a variable "' + name + '" that is not defined as an object in the map.'
);
} }
if (variableObject.writableBy && !user.tags.includes(variableObject.writableBy)) { if (variableObject.writableBy && !user.tags.includes(variableObject.writableBy)) {
throw new Error( throw new VariableError(
'Trying to set a variable "' + 'Trying to set a variable "' +
name + name +
'". User "' + '". User "' +

View File

@ -38,6 +38,7 @@ services:
JITSI_URL: $JITSI_URL JITSI_URL: $JITSI_URL
JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE" JITSI_PRIVATE_MODE: "$JITSI_PRIVATE_MODE"
PUSHER_URL: //pusher.${DOMAIN} PUSHER_URL: //pusher.${DOMAIN}
ICON_URL: //icon.${DOMAIN}
TURN_SERVER: "${TURN_SERVER}" TURN_SERVER: "${TURN_SERVER}"
TURN_USER: "${TURN_USER}" TURN_USER: "${TURN_USER}"
TURN_PASSWORD: "${TURN_PASSWORD}" TURN_PASSWORD: "${TURN_PASSWORD}"
@ -98,3 +99,15 @@ services:
- "traefik.http.routers.back-ssl.service=back" - "traefik.http.routers.back-ssl.service=back"
- "traefik.http.routers.back-ssl.tls.certresolver=myresolver" - "traefik.http.routers.back-ssl.tls.certresolver=myresolver"
restart: unless-stopped restart: unless-stopped
icon:
image: matthiasluedtke/iconserver:v3.13.0
labels:
- "traefik.http.routers.icon.rule=Host(`icon.${DOMAIN}`)"
- "traefik.http.routers.icon.entryPoints=web,traefik"
- "traefik.http.services.icon.loadbalancer.server.port=8080"
- "traefik.http.routers.icon-ssl.rule=Host(`icon.${DOMAIN}`)"
- "traefik.http.routers.icon-ssl.entryPoints=websecure"
- "traefik.http.routers.icon-ssl.tls=true"
- "traefik.http.routers.icon-ssl.service=icon"
- "traefik.http.routers.icon-ssl.tls.certresolver=myresolver"

View File

@ -30,6 +30,7 @@ services:
UPLOADER_URL: /uploader UPLOADER_URL: /uploader
ADMIN_URL: /admin ADMIN_URL: /admin
MAPS_URL: /maps MAPS_URL: /maps
ICON_URL: /icon
STARTUP_COMMAND_1: ./templater.sh STARTUP_COMMAND_1: ./templater.sh
STARTUP_COMMAND_2: yarn install STARTUP_COMMAND_2: yarn install
TURN_SERVER: "turn:localhost:3478,turns:localhost:5349" TURN_SERVER: "turn:localhost:3478,turns:localhost:5349"
@ -61,6 +62,8 @@ services:
environment: environment:
DEBUG: "*" DEBUG: "*"
STARTUP_COMMAND_1: yarn install STARTUP_COMMAND_1: yarn install
# wait for files generated by "messages" container to exists
STARTUP_COMMAND_2: while [ ! -f /usr/src/app/src/Messages/generated/messages_pb.js ]; do sleep 1; done
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY" SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
SECRET_KEY: yourSecretKey SECRET_KEY: yourSecretKey
ADMIN_API_TOKEN: "$ADMIN_API_TOKEN" ADMIN_API_TOKEN: "$ADMIN_API_TOKEN"
@ -121,6 +124,8 @@ services:
environment: environment:
DEBUG: "*" DEBUG: "*"
STARTUP_COMMAND_1: yarn install STARTUP_COMMAND_1: yarn install
# wait for files generated by "messages" container to exists
STARTUP_COMMAND_2: while [ ! -f /usr/src/app/src/Messages/generated/messages_pb.js ]; do sleep 1; done
SECRET_KEY: yourSecretKey SECRET_KEY: yourSecretKey
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY" SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
ALLOW_ARTILLERY: "true" ALLOW_ARTILLERY: "true"
@ -181,6 +186,20 @@ services:
redis: redis:
image: redis:6 image: redis:6
icon:
image: matthiasluedtke/iconserver:v3.13.0
labels:
- "traefik.http.middlewares.strip-icon-prefix.stripprefix.prefixes=/icon"
- "traefik.http.routers.icon.rule=PathPrefix(`/icon`)"
- "traefik.http.routers.icon.middlewares=strip-icon-prefix@docker"
- "traefik.http.routers.icon.entryPoints=web"
- "traefik.http.services.icon.loadbalancer.server.port=8080"
- "traefik.http.routers.icon-ssl.rule=PathPrefix(`/icon`)"
- "traefik.http.routers.icon-ssl.middlewares=strip-icon-prefix@docker"
- "traefik.http.routers.icon-ssl.entryPoints=websecure"
- "traefik.http.routers.icon-ssl.tls=true"
- "traefik.http.routers.icon-ssl.service=icon"
# coturn: # coturn:
# image: coturn/coturn:4.5.2 # image: coturn/coturn:4.5.2
# command: # command:

View File

@ -0,0 +1,12 @@
version: "3"
services:
testcafe:
image: testcafe/testcafe:1.17.1
working_dir: /tests
environment:
BROWSER: "chromium --use-fake-device-for-media-stream"
volumes:
- ./tests:/tests
- ./maps:/maps
# security_opt:
# - seccomp:unconfined

View File

@ -17,6 +17,12 @@ services:
- front - front
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
networks:
default:
aliases:
- 'play.workadventure.localhost'
- 'pusher.workadventure.localhost'
- 'maps.workadventure.localhost'
front: front:
image: thecodingmachine/nodejs:14 image: thecodingmachine/nodejs:14
@ -63,6 +69,8 @@ services:
environment: environment:
DEBUG: "socket:*" DEBUG: "socket:*"
STARTUP_COMMAND_1: yarn install STARTUP_COMMAND_1: yarn install
# wait for files generated by "messages" container to exists
STARTUP_COMMAND_2: while [ ! -f /usr/src/app/src/Messages/generated/messages_pb.js ]; do sleep 1; done
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY" SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
SECRET_KEY: yourSecretKey SECRET_KEY: yourSecretKey
ADMIN_API_TOKEN: "$ADMIN_API_TOKEN" ADMIN_API_TOKEN: "$ADMIN_API_TOKEN"
@ -117,6 +125,8 @@ services:
environment: environment:
DEBUG: "*" DEBUG: "*"
STARTUP_COMMAND_1: yarn install STARTUP_COMMAND_1: yarn install
# wait for files generated by "messages" container to exists
STARTUP_COMMAND_2: while [ ! -f /usr/src/app/src/Messages/generated/messages_pb.js ]; do sleep 1; done
SECRET_KEY: yourSecretKey SECRET_KEY: yourSecretKey
SECRET_JITSI_KEY: "$SECRET_JITSI_KEY" SECRET_JITSI_KEY: "$SECRET_JITSI_KEY"
ALLOW_ARTILLERY: "true" ALLOW_ARTILLERY: "true"
@ -127,6 +137,7 @@ services:
MAX_PER_GROUP: "MAX_PER_GROUP" MAX_PER_GROUP: "MAX_PER_GROUP"
REDIS_HOST: redis REDIS_HOST: redis
NODE_ENV: development NODE_ENV: development
STORE_VARIABLES_FOR_LOCAL_MAPS: "true"
volumes: volumes:
- ./back:/usr/src/app - ./back:/usr/src/app
labels: labels:

View File

@ -4,7 +4,7 @@ declare let window: any;
class AnalyticsClient { class AnalyticsClient {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
private posthogPromise: Promise<any>; private posthogPromise: Promise<any>|undefined;
constructor() { constructor() {
if (POSTHOG_API_KEY && POSTHOG_URL) { if (POSTHOG_API_KEY && POSTHOG_URL) {
@ -14,90 +14,78 @@ class AnalyticsClient {
window.posthog = posthog; window.posthog = posthog;
return posthog; return posthog;
}); });
} else {
this.posthogPromise = Promise.reject();
} }
} }
identifyUser(uuid: string, email: string | null) { identifyUser(uuid: string, email: string | null) {
this.posthogPromise this.posthogPromise
.then((posthog) => { ?.then((posthog) => {
posthog.identify(uuid, { uuid, email, wa: true }); posthog.identify(uuid, { uuid, email, wa: true });
}) });
.catch();
} }
loggedWithSso() { loggedWithSso() {
this.posthogPromise this.posthogPromise
.then((posthog) => { ?.then((posthog) => {
posthog.capture("wa-logged-sso"); posthog.capture("wa-logged-sso");
}) });
.catch();
} }
loggedWithToken() { loggedWithToken() {
this.posthogPromise this.posthogPromise
.then((posthog) => { ?.then((posthog) => {
posthog.capture("wa-logged-token"); posthog.capture("wa-logged-token");
}) });
.catch();
} }
enteredRoom(roomId: string, roomGroup: string | null) { enteredRoom(roomId: string, roomGroup: string | null) {
this.posthogPromise this.posthogPromise
.then((posthog) => { ?.then((posthog) => {
posthog.capture("$pageView", { roomId, roomGroup }); posthog.capture("$pageView", { roomId, roomGroup });
posthog.capture("enteredRoom"); posthog.capture("enteredRoom");
}) });
.catch();
} }
openedMenu() { openedMenu() {
this.posthogPromise this.posthogPromise
.then((posthog) => { ?.then((posthog) => {
posthog.capture("wa-opened-menu"); posthog.capture("wa-opened-menu");
}) });
.catch();
} }
launchEmote(emote: string) { launchEmote(emote: string) {
this.posthogPromise this.posthogPromise
.then((posthog) => { ?.then((posthog) => {
posthog.capture("wa-emote-launch", { emote }); posthog.capture("wa-emote-launch", { emote });
}) });
.catch();
} }
enteredJitsi(roomName: string, roomId: string) { enteredJitsi(roomName: string, roomId: string) {
this.posthogPromise this.posthogPromise
.then((posthog) => { ?.then((posthog) => {
posthog.capture("wa-entered-jitsi", { roomName, roomId }); posthog.capture("wa-entered-jitsi", { roomName, roomId });
}) });
.catch();
} }
validationName() { validationName() {
this.posthogPromise this.posthogPromise
.then((posthog) => { ?.then((posthog) => {
posthog.capture("wa-name-validation"); posthog.capture("wa-name-validation");
}) });
.catch();
} }
validationWoka(scene: string) { validationWoka(scene: string) {
this.posthogPromise this.posthogPromise
.then((posthog) => { ?.then((posthog) => {
posthog.capture("wa-woka-validation", { scene }); posthog.capture("wa-woka-validation", { scene });
}) });
.catch();
} }
validationVideo() { validationVideo() {
this.posthogPromise this.posthogPromise
.then((posthog) => { ?.then((posthog) => {
posthog.capture("wa-video-validation"); posthog.capture("wa-video-validation");
}) });
.catch();
} }
} }
export const analyticsClient = new AnalyticsClient(); export const analyticsClient = new AnalyticsClient();

View File

@ -321,8 +321,9 @@ class ConnectionManager {
this.localUser = new LocalUser(userUuid, textures, email); this.localUser = new LocalUser(userUuid, textures, email);
localUserStore.saveUser(this.localUser); localUserStore.saveUser(this.localUser);
this.authToken = authToken; this.authToken = authToken;
if (username) {
gameManager.setPlayerName(username); gameManager.setPlayerName(username);
}
//user connected, set connected store for menu at true //user connected, set connected store for menu at true
userIsConnected.set(true); userIsConnected.set(true);
} }

View File

@ -122,17 +122,22 @@ class LocalUserStore {
setLastRoomUrl(roomUrl: string): void { setLastRoomUrl(roomUrl: string): void {
localStorage.setItem(lastRoomUrl, roomUrl.toString()); localStorage.setItem(lastRoomUrl, roomUrl.toString());
if ('caches' in window) {
caches.open(cacheAPIIndex).then((cache) => { caches.open(cacheAPIIndex).then((cache) => {
const stringResponse = new Response(JSON.stringify({ roomUrl })); const stringResponse = new Response(JSON.stringify({ roomUrl }));
cache.put(`/${lastRoomUrl}`, stringResponse); cache.put(`/${lastRoomUrl}`, stringResponse);
}); });
} }
}
getLastRoomUrl(): string { getLastRoomUrl(): string {
return ( return (
localStorage.getItem(lastRoomUrl) ?? window.location.protocol + "//" + window.location.host + START_ROOM_URL localStorage.getItem(lastRoomUrl) ?? window.location.protocol + "//" + window.location.host + START_ROOM_URL
); );
} }
getLastRoomUrlCacheApi(): Promise<string | undefined> { getLastRoomUrlCacheApi(): Promise<string | undefined> {
if (!('caches' in window)) {
return Promise.resolve(undefined);
}
return caches.open(cacheAPIIndex).then((cache) => { return caches.open(cacheAPIIndex).then((cache) => {
return cache.match(`/${lastRoomUrl}`).then((res) => { return cache.match(`/${lastRoomUrl}`).then((res) => {
return res?.json().then((data) => { return res?.json().then((data) => {

View File

@ -162,6 +162,7 @@ export class EmbeddedWebsiteManager {
const iframe = document.createElement("iframe"); const iframe = document.createElement("iframe");
iframe.src = absoluteUrl; iframe.src = absoluteUrl;
iframe.tabIndex = -1;
iframe.style.width = embeddedWebsiteEvent.position.width + "px"; iframe.style.width = embeddedWebsiteEvent.position.width + "px";
iframe.style.height = embeddedWebsiteEvent.position.height + "px"; iframe.style.height = embeddedWebsiteEvent.position.height + "px";
iframe.style.margin = "0"; iframe.style.margin = "0";

View File

@ -52,13 +52,15 @@ export class StartPositionCalculator {
if (!selectedOrDefaultLayer) { if (!selectedOrDefaultLayer) {
selectedOrDefaultLayer = defaultStartLayerName; selectedOrDefaultLayer = defaultStartLayerName;
} }
selectedOrDefaultLayer = selectedOrDefaultLayer.replace('#','');
let foundLayer: ITiledMapLayer | null = null; let foundLayer: ITiledMapLayer | null = null;
for (const layer of this.gameMap.flatLayers) { for (const layer of this.gameMap.flatLayers) {
if (layer.type !== "tilelayer") continue; if (layer.type !== "tilelayer") continue;
//we want to prioritize the selectedLayer other the start layer //we want to prioritize the selectedLayer other the start layer
if ( if (
(selectedOrDefaultLayer === layer.name || (selectedOrDefaultLayer === layer.name ||
selectedOrDefaultLayer === `#${layer.name}` ||
layer.name.endsWith("/" + selectedOrDefaultLayer)) && layer.name.endsWith("/" + selectedOrDefaultLayer)) &&
layer.type === "tilelayer" && layer.type === "tilelayer" &&
(selectedOrDefaultLayer === defaultStartLayerName || this.isStartLayer(layer)) (selectedOrDefaultLayer === defaultStartLayerName || this.isStartLayer(layer))

1
maps/tests/Variables/Cache/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
variables_tmp.json

View File

@ -0,0 +1,5 @@
WA.onInit().then(async () => {
console.log('Trying to set variable "myvar". This should work, even if the cache was busted.');
await WA.state.saveVariable('myvar', {'foo': 'bar'});
console.log('SUCCESS!');
});

View File

@ -0,0 +1,82 @@
{ "compressionlevel":-1,
"height":10,
"infinite":false,
"layers":[
{
"data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
"height":10,
"id":1,
"name":"floor",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":10,
"id":2,
"name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"floorLayer",
"objects":[
{
"height":67,
"id":3,
"name":"",
"rotation":0,
"text":
{
"fontfamily":"Sans Serif",
"pixelsize":11,
"text":"Test:\nThis test is to be run automatically using testcafe",
"wrap":true
},
"type":"",
"visible":true,
"width":252.4375,
"x":2.78125,
"y":2.5
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":8,
"nextobjectid":11,
"orientation":"orthogonal",
"renderorder":"right-down",
"tiledversion":"2021.03.23",
"tileheight":32,
"tilesets":[
{
"columns":11,
"firstgid":1,
"image":"..\/..\/tileset1.png",
"imageheight":352,
"imagewidth":352,
"margin":0,
"name":"tileset1",
"spacing":0,
"tilecount":121,
"tileheight":32,
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":1.5,
"width":10
}

View File

@ -0,0 +1,126 @@
{ "compressionlevel":-1,
"height":10,
"infinite":false,
"layers":[
{
"data":[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
"height":10,
"id":1,
"name":"floor",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"data":[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
"height":10,
"id":2,
"name":"start",
"opacity":1,
"type":"tilelayer",
"visible":true,
"width":10,
"x":0,
"y":0
},
{
"draworder":"topdown",
"id":3,
"name":"floorLayer",
"objects":[
{
"height":67,
"id":3,
"name":"",
"rotation":0,
"text":
{
"fontfamily":"Sans Serif",
"pixelsize":11,
"text":"Test:\nThis test is to be run automatically using testcafe\n\n(2nd file)",
"wrap":true
},
"type":"",
"visible":true,
"width":252.4375,
"x":2.78125,
"y":2.5
},
{
"height":0,
"id":9,
"name":"myvar",
"point":true,
"properties":[
{
"name":"default",
"type":"string",
"value":"{}"
},
{
"name":"jsonSchema",
"type":"string",
"value":"{}"
},
{
"name":"persist",
"type":"bool",
"value":true
},
{
"name":"readableBy",
"type":"string",
"value":""
},
{
"name":"writableBy",
"type":"string",
"value":""
}],
"rotation":0,
"type":"variable",
"visible":true,
"width":0,
"x":88.8149900876127,
"y":147.75212636695
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":8,
"nextobjectid":11,
"orientation":"orthogonal",
"properties":[
{
"name":"script",
"type":"string",
"value":"script.js"
}],
"renderorder":"right-down",
"tiledversion":"2021.03.23",
"tileheight":32,
"tilesets":[
{
"columns":11,
"firstgid":1,
"image":"..\/..\/tileset1.png",
"imageheight":352,
"imagewidth":352,
"margin":0,
"name":"tileset1",
"spacing":0,
"tilecount":121,
"tileheight":32,
"tilewidth":32
}],
"tilewidth":32,
"type":"map",
"version":1.5,
"width":10
}

1
tests/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/node_modules

13
tests/.testcaferc.js Normal file
View File

@ -0,0 +1,13 @@
const BROWSER = process.env.BROWSER || "chrome --use-fake-ui-for-media-stream --use-fake-device-for-media-stream";
module.exports = {
"browsers": BROWSER,
"hostname": "localhost",
//"skipJsErrors": true,
"src": "tests/",
"screenshots": {
"path": "screenshots/",
"takeOnFails": true,
"thumbnails": false,
}
}

18
tests/README.md Normal file
View File

@ -0,0 +1,18 @@
End-to-end tests
This directory contains automated end to end tests.
To run them locally:
```console
$ npm install
$ npm test
```
Alternatively, you can use docker-compose to run the tests:
```console
$ docker-compose -f docker-compose.testcafe.yml up --exit-code-from testcafe
```
Note: by default, tests are running in Chrome locally and in Chromium in the Docker image.

8915
tests/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

8
tests/package.json Normal file
View File

@ -0,0 +1,8 @@
{
"devDependencies": {
"testcafe": "^1.17.1"
},
"scripts": {
"test": "testcafe"
}
}

1
tests/screenshots/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*

67
tests/tests/test.ts Normal file
View File

@ -0,0 +1,67 @@
const fs = require('fs')
fixture `Variables`
.page `http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/Cache/variables_tmp.json`;
test("Test that variables cache in the back don't prevent setting a variable in case the map changes", async t => {
// 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');
await t
.typeText('input[name="loginSceneName"]', 'foo')
.click('button.loginSceneFormSubmit')
.click('button.selectCharacterButtonRight')
.click('button.selectCharacterButtonRight')
.click('button.selectCharacterSceneFormSubmit')
.click('button.letsgo');
//.takeScreenshot('before_switch.png');
// Let's REPLACE the map by a map that has a new variable
// At this point, the back server contains a cache of the old map (with no variables)
fs.copyFileSync('../maps/tests/Variables/Cache/variables_cache_2.json', '../maps/tests/Variables/Cache/variables_tmp.json');
await t.openWindow('http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/Cache/variables_tmp.json');
await t.resizeWindow(960, 800);
await t
.typeText('input[name="loginSceneName"]', 'foo')
.click('button.loginSceneFormSubmit')
.click('button.selectCharacterButtonRight')
.click('button.selectCharacterButtonRight')
.click('button.selectCharacterSceneFormSubmit')
.click('button.letsgo');
//.takeScreenshot('after_switch.png');
const messages = await t.getBrowserConsoleMessages();
const logs = messages['log'];
// Let's check we successfully manage to save the variable value.
await assertLogMessage(t, 'SUCCESS!');
t.ctx.passed = true;
}).after(async t => {
if (!t.ctx.passed) {
console.log("Test failed. Browser logs:")
console.log(await t.getBrowserConsoleMessages());
}
});
/**
* Tries to find a given log message in the logs (for 10 seconds)
*/
async function assertLogMessage(t, message: string): Promise<void> {
let i = 0;
let logs: string[]|undefined;
do {
const messages = await t.getBrowserConsoleMessages();
logs = messages['log'];
if (logs.find((str) => str === message)) {
break;
}
await t.wait(1000);
i++;
} while (i < 10);
await t.expect(logs).contains(message);
}