Merge pull request #1602 from thecodingmachine/e2e_reconnect_tests

Adding a reconnect feature in case first Pusher request fails
This commit is contained in:
David Négrier 2021-12-06 18:44:18 +01:00 committed by GitHub
commit 6a8717c22f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1580 additions and 113 deletions

View File

@ -20,6 +20,15 @@ jobs:
- name: "Checkout" - name: "Checkout"
uses: "actions/checkout@v2.0.0" uses: "actions/checkout@v2.0.0"
- name: "Setup NodeJS"
uses: actions/setup-node@v1
with:
node-version: '14.x'
- name: "Install dependencies"
run: npm install
working-directory: "tests"
- name: "Setup .env file" - name: "Setup .env file"
run: cp .env.template .env run: cp .env.template .env
@ -27,10 +36,10 @@ jobs:
run: sudo chown 1000:1000 -R . run: sudo chown 1000:1000 -R .
- name: "Start environment" - name: "Start environment"
run: docker-compose up -d run: LIVE_RELOAD=0 docker-compose up -d
- name: "Wait for environment to build (and downloading testcafe image)" - 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 -q "Compiled successfully" run: (docker-compose -f docker-compose.testcafe.yml build &) && docker-compose logs -f --tail=0 front | grep -q "Compiled successfully"
# - name: "temp debug: display logs" # - name: "temp debug: display logs"
# run: docker-compose logs # run: docker-compose logs
@ -42,7 +51,7 @@ jobs:
# run: docker-compose logs -f pusher | grep -q "WorkAdventure starting on port" # run: docker-compose logs -f pusher | grep -q "WorkAdventure starting on port"
- name: "Run tests" - name: "Run tests"
run: docker-compose -f docker-compose.testcafe.yml up --exit-code-from testcafe run: PROJECT_DIR=$(pwd) docker-compose -f docker-compose.testcafe.yml up --exit-code-from testcafe
- name: Upload failed tests - name: Upload failed tests
if: ${{ failure() }} if: ${{ failure() }}

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:
$ PROJECT_DIR=$(pwd) docker-compose -f docker-compose.testcafe.yml up
```

View File

@ -12,43 +12,52 @@ export class DebugController {
getDump() { getDump() {
this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => { this.App.get("/dump", (res: HttpResponse, req: HttpRequest) => {
const query = parse(req.getQuery()); (async () => {
const query = parse(req.getQuery());
if (query.token !== ADMIN_API_TOKEN) { if (query.token !== ADMIN_API_TOKEN) {
return res.writeStatus("401 Unauthorized").end("Invalid token sent!"); return res.writeStatus("401 Unauthorized").end("Invalid token sent!");
} }
return res return res
.writeStatus("200 OK") .writeStatus("200 OK")
.writeHeader("Content-Type", "application/json") .writeHeader("Content-Type", "application/json")
.end( .end(
stringify(socketManager.getWorlds(), (key: unknown, value: unknown) => { stringify(
if (key === "listeners") { await Promise.all(socketManager.getWorlds().values()),
return "Listeners"; (key: unknown, value: unknown) => {
} if (key === "listeners") {
if (key === "socket") { return "Listeners";
return "Socket"; }
} if (key === "socket") {
if (key === "batchedMessages") { return "Socket";
return "BatchedMessages"; }
} if (key === "batchedMessages") {
if (value instanceof Map) { return "BatchedMessages";
const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any }
for (const [mapKey, mapValue] of value.entries()) { if (value instanceof Map) {
obj[mapKey] = mapValue; const obj: any = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
for (const [mapKey, mapValue] of value.entries()) {
obj[mapKey] = mapValue;
}
return obj;
} else if (value instanceof Set) {
const obj: Array<unknown> = [];
for (const [setKey, setValue] of value.entries()) {
obj.push(setValue);
}
return obj;
} else {
return value;
}
} }
return obj; )
} else if (value instanceof Set) { );
const obj: Array<unknown> = []; })().catch((e) => {
for (const [setKey, setValue] of value.entries()) { console.error(e);
obj.push(setValue); res.writeStatus("500");
} res.end("An error occurred");
return obj; });
} else {
return value;
}
})
);
}); });
} }
} }

View File

@ -1,12 +1,19 @@
version: "3" version: "3.5"
services: services:
testcafe: testcafe:
image: testcafe/testcafe:1.17.1 build: tests/
working_dir: /tests working_dir: /project/tests
command:
- --dev
# Run as root to have the right to access /var/run/docker.sock
user: root
environment: environment:
BROWSER: "chromium --use-fake-device-for-media-stream" BROWSER: "chromium --use-fake-device-for-media-stream"
PROJECT_DIR: ${PROJECT_DIR}
ADMIN_API_TOKEN: ${ADMIN_API_TOKEN}
volumes: volumes:
- ./tests:/tests - ./:/project
- ./maps:/maps - ./maps:/maps
- /var/run/docker.sock:/var/run/docker.sock
# security_opt: # security_opt:
# - seccomp:unconfined # - seccomp:unconfined

View File

@ -1,20 +1,21 @@
version: "3" version: "3.5"
services: services:
reverse-proxy: reverse-proxy:
image: traefik:v2.0 image: traefik:v2.5
command: command:
- --api.insecure=true - --api.insecure=true
- --providers.docker - --providers.docker
- --entryPoints.web.address=:80 - --entryPoints.web.address=:80
- --entryPoints.websecure.address=:443 - --entryPoints.websecure.address=:443
- "--providers.docker.exposedbydefault=false"
ports: ports:
- "80:80" - "80:80"
- "443:443" - "443:443"
# The Web UI (enabled by --api.insecure=true) # The Web UI (enabled by --api.insecure=true)
- "8080:8080" - "8080:8080"
depends_on: #depends_on:
- back # - back
- front # - front
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
networks: networks:
@ -51,10 +52,12 @@ services:
MAX_USERNAME_LENGTH: "$MAX_USERNAME_LENGTH" MAX_USERNAME_LENGTH: "$MAX_USERNAME_LENGTH"
DISABLE_ANONYMOUS: "$DISABLE_ANONYMOUS" DISABLE_ANONYMOUS: "$DISABLE_ANONYMOUS"
OPID_LOGIN_SCREEN_PROVIDER: "$OPID_LOGIN_SCREEN_PROVIDER" OPID_LOGIN_SCREEN_PROVIDER: "$OPID_LOGIN_SCREEN_PROVIDER"
LIVE_RELOAD: "$LIVE_RELOAD:-true"
command: yarn run start command: yarn run start
volumes: volumes:
- ./front:/usr/src/app - ./front:/usr/src/app
labels: labels:
- "traefik.enable=true"
- "traefik.http.routers.front.rule=Host(`play.workadventure.localhost`)" - "traefik.http.routers.front.rule=Host(`play.workadventure.localhost`)"
- "traefik.http.routers.front.entryPoints=web" - "traefik.http.routers.front.entryPoints=web"
- "traefik.http.services.front.loadbalancer.server.port=8080" - "traefik.http.services.front.loadbalancer.server.port=8080"
@ -87,6 +90,7 @@ services:
volumes: volumes:
- ./pusher:/usr/src/app - ./pusher:/usr/src/app
labels: labels:
- "traefik.enable=true"
- "traefik.http.routers.pusher.rule=Host(`pusher.workadventure.localhost`)" - "traefik.http.routers.pusher.rule=Host(`pusher.workadventure.localhost`)"
- "traefik.http.routers.pusher.entryPoints=web" - "traefik.http.routers.pusher.entryPoints=web"
- "traefik.http.services.pusher.loadbalancer.server.port=8080" - "traefik.http.services.pusher.loadbalancer.server.port=8080"
@ -111,6 +115,7 @@ services:
volumes: volumes:
- ./maps:/var/www/html - ./maps:/var/www/html
labels: labels:
- "traefik.enable=true"
- "traefik.http.routers.maps.rule=Host(`maps.workadventure.localhost`)" - "traefik.http.routers.maps.rule=Host(`maps.workadventure.localhost`)"
- "traefik.http.routers.maps.entryPoints=web,traefik" - "traefik.http.routers.maps.entryPoints=web,traefik"
- "traefik.http.services.maps.loadbalancer.server.port=80" - "traefik.http.services.maps.loadbalancer.server.port=80"
@ -142,6 +147,7 @@ services:
volumes: volumes:
- ./back:/usr/src/app - ./back:/usr/src/app
labels: labels:
- "traefik.enable=true"
- "traefik.http.routers.back.rule=Host(`api.workadventure.localhost`)" - "traefik.http.routers.back.rule=Host(`api.workadventure.localhost`)"
- "traefik.http.routers.back.entryPoints=web" - "traefik.http.routers.back.entryPoints=web"
- "traefik.http.services.back.loadbalancer.server.port=8080" - "traefik.http.services.back.loadbalancer.server.port=8080"
@ -160,6 +166,7 @@ services:
volumes: volumes:
- ./uploader:/usr/src/app - ./uploader:/usr/src/app
labels: labels:
- "traefik.enable=true"
- "traefik.http.routers.uploader.rule=Host(`uploader.workadventure.localhost`)" - "traefik.http.routers.uploader.rule=Host(`uploader.workadventure.localhost`)"
- "traefik.http.routers.uploader.entryPoints=web" - "traefik.http.routers.uploader.entryPoints=web"
- "traefik.http.services.uploader.loadbalancer.server.port=8080" - "traefik.http.services.uploader.loadbalancer.server.port=8080"
@ -187,6 +194,7 @@ services:
redisinsight: redisinsight:
image: redislabs/redisinsight:latest image: redislabs/redisinsight:latest
labels: labels:
- "traefik.enable=true"
- "traefik.http.routers.redisinsight.rule=Host(`redis.workadventure.localhost`)" - "traefik.http.routers.redisinsight.rule=Host(`redis.workadventure.localhost`)"
- "traefik.http.routers.redisinsight.entryPoints=web" - "traefik.http.routers.redisinsight.entryPoints=web"
- "traefik.http.services.redisinsight.loadbalancer.server.port=8001" - "traefik.http.services.redisinsight.loadbalancer.server.port=8001"
@ -198,6 +206,7 @@ services:
icon: icon:
image: matthiasluedtke/iconserver:v3.13.0 image: matthiasluedtke/iconserver:v3.13.0
labels: labels:
- "traefik.enable=true"
- "traefik.http.routers.icon.rule=Host(`icon.workadventure.localhost`)" - "traefik.http.routers.icon.rule=Host(`icon.workadventure.localhost`)"
- "traefik.http.routers.icon.entryPoints=web" - "traefik.http.routers.icon.entryPoints=web"
- "traefik.http.services.icon.loadbalancer.server.port=8080" - "traefik.http.services.icon.loadbalancer.server.port=8080"

View File

@ -56,6 +56,7 @@
"queue-typescript": "^1.0.1", "queue-typescript": "^1.0.1",
"quill": "1.3.6", "quill": "1.3.6",
"quill-delta-to-html": "^0.12.0", "quill-delta-to-html": "^0.12.0",
"retry-axios": "^2.6.0",
"rxjs": "^6.6.3", "rxjs": "^6.6.3",
"simple-peer": "^9.11.0", "simple-peer": "^9.11.0",
"socket.io-client": "^2.3.0", "socket.io-client": "^2.3.0",

View File

@ -1,8 +1,8 @@
<script lang="ts"> <script lang="ts">
import { errorStore } from "../../Stores/ErrorStore"; import { errorStore, hasClosableMessagesInErrorStore } from "../../Stores/ErrorStore";
function close(): boolean { function close(): boolean {
errorStore.clearMessages(); errorStore.clearClosableMessages();
return false; return false;
} }
</script> </script>
@ -11,12 +11,14 @@
<p class="nes-text is-error title">Error</p> <p class="nes-text is-error title">Error</p>
<div class="body"> <div class="body">
{#each $errorStore as error} {#each $errorStore as error}
<p>{error}</p> <p>{error.message}</p>
{/each} {/each}
</div> </div>
<div class="button-bar"> {#if $hasClosableMessagesInErrorStore}
<button class="nes-btn is-error" on:click={close}>Close</button> <div class="button-bar">
</div> <button class="nes-btn is-error" on:click={close}>Close</button>
</div>
{/if}
</div> </div>
<style lang="scss"> <style lang="scss">

View File

@ -0,0 +1,37 @@
import axios from "axios";
import * as rax from "retry-axios";
import { errorStore } from "../Stores/ErrorStore";
/**
* This instance of Axios will retry in case of an issue and display an error message as a HTML overlay.
*/
export const axiosWithRetry = axios.create();
axiosWithRetry.defaults.raxConfig = {
instance: axiosWithRetry,
retry: Infinity,
noResponseRetries: Infinity,
maxRetryAfter: 60_000,
// You can detect when a retry is happening, and figure out how many
// retry attempts have been made
onRetryAttempt: (err) => {
const cfg = rax.getConfig(err);
console.log(err);
console.log(cfg);
console.log(`Retry attempt #${cfg?.currentRetryAttempt} on URL '${err.config.url}'`);
errorStore.addErrorMessage("Unable to connect to WorkAdventure. Are you connected to internet?", {
closable: false,
id: "axios_retry",
});
},
};
axiosWithRetry.interceptors.response.use((res) => {
if (res.status < 400) {
errorStore.clearMessageById("axios_retry");
}
return res;
});
const interceptorId = rax.attach(axiosWithRetry);

View File

@ -10,6 +10,7 @@ import { _ServiceWorker } from "../Network/ServiceWorker";
import { loginSceneVisibleIframeStore } from "../Stores/LoginSceneStore"; import { loginSceneVisibleIframeStore } from "../Stores/LoginSceneStore";
import { userIsConnected } from "../Stores/MenuStore"; import { userIsConnected } from "../Stores/MenuStore";
import { analyticsClient } from "../Administration/AnalyticsClient"; import { analyticsClient } from "../Administration/AnalyticsClient";
import { axiosWithRetry } from "./AxiosUtils";
class ConnectionManager { class ConnectionManager {
private localUser!: LocalUser; private localUser!: LocalUser;
@ -232,7 +233,7 @@ class ConnectionManager {
} }
public async anonymousLogin(isBenchmark: boolean = false): Promise<void> { public async anonymousLogin(isBenchmark: boolean = false): Promise<void> {
const data = await Axios.post(`${PUSHER_URL}/anonymLogin`).then((res) => res.data); const data = await axiosWithRetry.post(`${PUSHER_URL}/anonymLogin`).then((res) => res.data);
this.localUser = new LocalUser(data.userUuid, [], data.email); this.localUser = new LocalUser(data.userUuid, [], data.email);
this.authToken = data.authToken; this.authToken = data.authToken;
if (!isBenchmark) { if (!isBenchmark) {

View File

@ -1,7 +1,10 @@
import * as rax from "retry-axios";
import Axios from "axios"; import Axios from "axios";
import { CONTACT_URL, PUSHER_URL, DISABLE_ANONYMOUS, OPID_LOGIN_SCREEN_PROVIDER } from "../Enum/EnvironmentVariable"; import { CONTACT_URL, PUSHER_URL, DISABLE_ANONYMOUS, OPID_LOGIN_SCREEN_PROVIDER } from "../Enum/EnvironmentVariable";
import type { CharacterTexture } from "./LocalUser"; import type { CharacterTexture } from "./LocalUser";
import { localUserStore } from "./LocalUserStore"; import { localUserStore } from "./LocalUserStore";
import axios from "axios";
import { axiosWithRetry } from "./AxiosUtils";
export class MapDetail { export class MapDetail {
constructor(public readonly mapUrl: string, public readonly textures: CharacterTexture[] | undefined) {} constructor(public readonly mapUrl: string, public readonly textures: CharacterTexture[] | undefined) {}
@ -90,7 +93,7 @@ export class Room {
private async getMapDetail(): Promise<MapDetail | RoomRedirect> { private async getMapDetail(): Promise<MapDetail | RoomRedirect> {
try { try {
const result = await Axios.get(`${PUSHER_URL}/map`, { const result = await axiosWithRetry.get(`${PUSHER_URL}/map`, {
params: { params: {
playUri: this.roomUrl.toString(), playUri: this.roomUrl.toString(),
authToken: localUserStore.getAuthToken(), authToken: localUserStore.getAuthToken(),

View File

@ -255,6 +255,9 @@ export class RoomConnection implements RoomConnection {
warningContainerStore.activateWarningContainer(); warningContainerStore.activateWarningContainer();
} else if (message.hasRefreshroommessage()) { } else if (message.hasRefreshroommessage()) {
//todo: implement a way to notify the user the room was refreshed. //todo: implement a way to notify the user the room was refreshed.
} else if (message.hasErrormessage()) {
const errorMessage = message.getErrormessage() as ErrorMessage;
console.error("An error occurred server side: " + errorMessage.getMessage());
} else { } else {
throw new Error("Unknown message received"); throw new Error("Unknown message received");
} }

View File

@ -269,7 +269,9 @@ export class GameScene extends DirtyScene {
// 127.0.0.1, localhost and *.localhost are considered secure, even on HTTP. // 127.0.0.1, localhost and *.localhost are considered secure, even on HTTP.
// So if we are in https, we can still try to load a HTTP local resource (can be useful for testing purposes) // So if we are in https, we can still try to load a HTTP local resource (can be useful for testing purposes)
// See https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure // See https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts#when_is_a_context_considered_secure
const url = new URL(file.src); const base = new URL(window.location.href);
base.pathname = "";
const url = new URL(file.src, base.toString());
const host = url.host.split(":")[0]; const host = url.host.split(":")[0];
if ( if (
window.location.protocol === "https:" && window.location.protocol === "https:" &&
@ -322,7 +324,6 @@ export class GameScene extends DirtyScene {
this.onMapLoad(data); this.onMapLoad(data);
} }
this.load.bitmapFont("main_font", "resources/fonts/arcade.png", "resources/fonts/arcade.xml");
//eslint-disable-next-line @typescript-eslint/no-explicit-any //eslint-disable-next-line @typescript-eslint/no-explicit-any
(this.load as any).rexWebFont({ (this.load as any).rexWebFont({
custom: { custom: {

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

@ -4,6 +4,7 @@ import Sprite = Phaser.GameObjects.Sprite;
import Text = Phaser.GameObjects.Text; import Text = Phaser.GameObjects.Text;
import ScenePlugin = Phaser.Scenes.ScenePlugin; import ScenePlugin = Phaser.Scenes.ScenePlugin;
import { WAError } from "./WAError"; import { WAError } from "./WAError";
import Axios from "axios";
export const ErrorSceneName = "ErrorScene"; export const ErrorSceneName = "ErrorScene";
enum Textures { enum Textures {
@ -36,7 +37,11 @@ export class ErrorScene extends Phaser.Scene {
preload() { preload() {
this.load.image(Textures.icon, "static/images/favicons/favicon-32x32.png"); this.load.image(Textures.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 // Note: arcade.png from the Phaser 3 examples at: https://github.com/photonstorm/phaser3-examples/tree/master/public/assets/fonts/bitmap
this.load.bitmapFont(Textures.mainFont, "resources/fonts/arcade.png", "resources/fonts/arcade.xml"); if (!this.cache.bitmapFont.has("main_font")) {
// We put this inside a "if" because despite the cache, Phaser will make a query to the XML file. And if there is no connection (which
// is not unlikely given the fact we are in an error scene), this will cause an error.
this.load.bitmapFont(Textures.mainFont, "resources/fonts/arcade.png", "resources/fonts/arcade.xml");
}
this.load.spritesheet("cat", "resources/characters/pipoya/Cat 01-1.png", { frameWidth: 32, frameHeight: 32 }); this.load.spritesheet("cat", "resources/characters/pipoya/Cat 01-1.png", { frameWidth: 32, frameHeight: 32 });
} }
@ -71,9 +76,9 @@ export class ErrorScene extends Phaser.Scene {
/** /**
* Displays the error page, with an error message matching the "error" parameters passed in. * Displays the error page, with an error message matching the "error" parameters passed in.
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any public static showError(error: unknown, scene: ScenePlugin): void {
public static showError(error: any, scene: ScenePlugin): void {
console.error(error); console.error(error);
console.trace();
if (typeof error === "string" || error instanceof String) { if (typeof error === "string" || error instanceof String) {
scene.start(ErrorSceneName, { scene.start(ErrorSceneName, {
@ -86,9 +91,10 @@ export class ErrorScene extends Phaser.Scene {
subTitle: error.subTitle, subTitle: error.subTitle,
message: error.details, message: error.details,
}); });
} else if (error.response) { } else if (Axios.isAxiosError(error) && error.response) {
// Axios HTTP error // Axios HTTP error
// client received an error response (5xx, 4xx) // client received an error response (5xx, 4xx)
console.error("Axios error. Request:", error.request, " - Response: ", error.response);
scene.start(ErrorSceneName, { scene.start(ErrorSceneName, {
title: title:
"HTTP " + "HTTP " +
@ -98,9 +104,10 @@ export class ErrorScene extends Phaser.Scene {
subTitle: "An error occurred while accessing URL:", subTitle: "An error occurred while accessing URL:",
message: error.response.config.url, message: error.response.config.url,
}); });
} else if (error.request) { } else if (Axios.isAxiosError(error)) {
// Axios HTTP error // Axios HTTP error
// client never received a response, or request never left // client never received a response, or request never left
console.error("Axios error. No full HTTP response received. Request to URL:", error.config.url);
scene.start(ErrorSceneName, { scene.start(ErrorSceneName, {
title: "Network error", title: "Network error",
subTitle: error.message, subTitle: error.message,

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",
} }
@ -41,7 +41,7 @@ export class ReconnectingScene extends Phaser.Scene {
"Connection lost. Reconnecting..." "Connection lost. Reconnecting..."
); );
const cat = this.physics.add.sprite(this.game.renderer.width / 2, this.game.renderer.height / 2 - 32, "cat"); const cat = this.add.sprite(this.game.renderer.width / 2, this.game.renderer.height / 2 - 32, "cat");
this.anims.create({ this.anims.create({
key: "right", key: "right",
frames: this.anims.generateFrameNumbers("cat", { start: 6, end: 8 }), frames: this.anims.generateFrameNumbers("cat", { start: 6, end: 8 }),

View File

@ -1,15 +1,27 @@
import { writable } from "svelte/store"; import { derived, writable } from "svelte/store";
interface ErrorMessage {
id: string | undefined;
closable: boolean; // Whether it can be closed by a user action or not
message: string | number | boolean | undefined;
}
/** /**
* A store that contains a list of error messages to be displayed. * A store that contains a list of error messages to be displayed.
*/ */
function createErrorStore() { function createErrorStore() {
const { subscribe, set, update } = writable<string[]>([]); const { subscribe, set, update } = writable<ErrorMessage[]>([]);
return { return {
subscribe, subscribe,
addErrorMessage: (e: string | Error): void => { addErrorMessage: (
update((messages: string[]) => { e: string | Error,
options?: {
closable?: boolean;
id?: string;
}
): void => {
update((messages: ErrorMessage[]) => {
let message: string; let message: string;
if (e instanceof Error) { if (e instanceof Error) {
message = e.message; message = e.message;
@ -17,17 +29,35 @@ function createErrorStore() {
message = e; message = e;
} }
if (!messages.includes(message)) { if (!messages.find((errorMessage) => errorMessage.message === message)) {
messages.push(message); messages.push({
message,
closable: options?.closable ?? true,
id: options?.id,
});
} }
return messages; return messages;
}); });
}, },
clearMessages: (): void => { clearMessageById: (id: string): void => {
set([]); update((messages: ErrorMessage[]) => {
messages = messages.filter((message) => message.id !== id);
return messages;
});
},
clearClosableMessages: (): void => {
update((messages: ErrorMessage[]) => {
messages = messages.filter((message) => message.closable);
return messages;
});
}, },
}; };
} }
export const errorStore = createErrorStore(); export const errorStore = createErrorStore();
export const hasClosableMessagesInErrorStore = derived(errorStore, ($errorStore) => {
const closableMessage = $errorStore.find((errorMessage) => errorMessage.closable);
return !!closableMessage;
});

View File

@ -32,6 +32,7 @@ module.exports = {
rewrites: [{ from: /^_\/.*$/, to: "/index.html" }], rewrites: [{ from: /^_\/.*$/, to: "/index.html" }],
disableDotRule: true, disableDotRule: true,
}, },
liveReload: process.env.LIVE_RELOAD != "0" && process.env.LIVE_RELOAD != "false",
}, },
module: { module: {
rules: [ rules: [

View File

@ -4872,6 +4872,11 @@ ret@~0.1.10:
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg== integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
retry-axios@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/retry-axios/-/retry-axios-2.6.0.tgz#d4dc5c8a8e73982e26a705e46a33df99a28723e0"
integrity sha512-pOLi+Gdll3JekwuFjXO3fTq+L9lzMQGcSq7M5gIjExcl3Gu1hd4XXuf5o3+LuSBsaULQH7DiNbsqPd1chVpQGQ==
retry@^0.12.0: retry@^0.12.0:
version "0.12.0" version "0.12.0"
resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" resolved "https://registry.yarnpkg.com/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b"

View File

@ -0,0 +1,33 @@
WA.onInit().then(() => {
console.log('Trying to read variable "doorOpened" whose default property is true. This should display "true".');
console.log('doorOpened', WA.state.loadVariable('doorOpened'));
console.log('Trying to set variable "not_exists". This should display an error in the console, followed by a log saying the error was caught.')
WA.state.saveVariable('not_exists', 'foo').catch((e) => {
console.log('Successfully caught error: ', e);
});
console.log('Trying to set variable "myvar". This should work.');
WA.state.saveVariable('myvar', {'foo': 'bar'});
console.log('Trying to read variable "myvar". This should display a {"foo": "bar"} object.');
console.log(WA.state.loadVariable('myvar'));
console.log('Trying to set variable "myvar" using proxy. This should work.');
WA.state.myvar = {'baz': 42};
console.log('Trying to read variable "myvar" using proxy. This should display a {"baz": 42} object.');
console.log(WA.state.myvar);
console.log('Trying to set variable "config". This should not work because we are not logged as admin.');
WA.state.saveVariable('config', {'foo': 'bar'}).catch(e => {
console.log('Successfully caught error because variable "config" is not writable: ', e);
});
console.log('Trying to read variable "readableByAdmin" that can only be read by "admin". We are not admin so we should not get the default value.');
if (WA.state.readableByAdmin === true) {
console.error('Failed test: readableByAdmin can be read.');
} else {
console.log('Success test: readableByAdmin was not read.');
}
});

View File

@ -0,0 +1,47 @@
<!doctype html>
<html lang="en">
<head>
<script>
var script = document.createElement('script');
// Don't do this at home kids! The "document.referrer" part is actually inserting a XSS security.
// We are OK in this precise case because the HTML page is hosted on the "maps" domain that contains only static files.
document.head.appendChild(script);
window.addEventListener('load', () => {
console.log('On load');
WA.onInit().then(() => {
console.log('After WA init');
const textField = document.getElementById('textField');
textField.value = WA.state.textField;
textField.addEventListener('change', function (evt) {
console.log('saving variable')
WA.state.textField = this.value;
});
WA.state.onVariableChange('textField').subscribe((value) => {
console.log('variable changed received')
textField.value = value;
});
document.getElementById('btn').addEventListener('click', () => {
console.log(WA.state.loadVariable('textField'));
document.getElementById('placeholder').innerText = WA.state.loadVariable('textField');
});
document.getElementById('setUndefined').addEventListener('click', () => {
WA.state.textField = undefined;
document.getElementById('textField').value = '';
});
});
})
</script>
</head>
<body>
<input type="text" id="textField" />
<button id="setUndefined">Delete variable</button>
<button id="btn">Display textField variable value</button>
<div id="placeholder"></div>
</body>
</html>

View File

@ -0,0 +1,131 @@
{ "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,
"properties":[
{
"name":"openWebsite",
"type":"string",
"value":"shared_variables.html"
},
{
"name":"openWebsiteAllowApi",
"type":"bool",
"value":true
}],
"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:\nChange the form\nConnect with another user\n\nResult:\nThe form should open in the same state for the other user\nAlso, a change on one user is directly propagated to the other user",
"wrap":true
},
"type":"",
"visible":true,
"width":252.4375,
"x":2.78125,
"y":2.5
},
{
"height":0,
"id":5,
"name":"textField",
"point":true,
"properties":[
{
"name":"default",
"type":"string",
"value":"default 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":57.5,
"y":111
}],
"opacity":1,
"type":"objectgroup",
"visible":true,
"x":0,
"y":0
}],
"nextlayerid":8,
"nextobjectid":10,
"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

@ -274,12 +274,12 @@ message ServerToClientMessage {
SendJitsiJwtMessage sendJitsiJwtMessage = 11; SendJitsiJwtMessage sendJitsiJwtMessage = 11;
SendUserMessage sendUserMessage = 12; SendUserMessage sendUserMessage = 12;
BanUserMessage banUserMessage = 13; BanUserMessage banUserMessage = 13;
AdminRoomMessage adminRoomMessage = 14; //AdminRoomMessage adminRoomMessage = 14;
WorldFullWarningMessage worldFullWarningMessage = 15; WorldFullWarningMessage worldFullWarningMessage = 15;
WorldFullMessage worldFullMessage = 16; WorldFullMessage worldFullMessage = 16;
RefreshRoomMessage refreshRoomMessage = 17; RefreshRoomMessage refreshRoomMessage = 17;
WorldConnexionMessage worldConnexionMessage = 18; WorldConnexionMessage worldConnexionMessage = 18;
EmoteEventMessage emoteEventMessage = 19; //EmoteEventMessage emoteEventMessage = 19;
TokenExpiredMessage tokenExpiredMessage = 20; TokenExpiredMessage tokenExpiredMessage = 20;
} }
} }

View File

@ -26,7 +26,7 @@ import { jwtTokenManager, tokenInvalidException } from "../Services/JWTTokenMana
import { adminApi, FetchMemberDataByUuidResponse } from "../Services/AdminApi"; import { adminApi, FetchMemberDataByUuidResponse } from "../Services/AdminApi";
import { SocketManager, socketManager } from "../Services/SocketManager"; import { SocketManager, socketManager } from "../Services/SocketManager";
import { emitInBatch } from "../Services/IoSocketHelpers"; import { emitInBatch } from "../Services/IoSocketHelpers";
import { ADMIN_SOCKETS_TOKEN, ADMIN_API_URL, DISABLE_ANONYMOUS, SOCKET_IDLE_TIMER } from "../Enum/EnvironmentVariable"; import { ADMIN_API_URL, DISABLE_ANONYMOUS, SOCKET_IDLE_TIMER } from "../Enum/EnvironmentVariable";
import { Zone } from "_Model/Zone"; import { Zone } from "_Model/Zone";
import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface"; import { ExAdminSocketInterface } from "_Model/Websocket/ExAdminSocketInterface";
import { CharacterTexture } from "../Services/AdminApi/CharacterTexture"; import { CharacterTexture } from "../Services/AdminApi/CharacterTexture";

View File

@ -1,8 +1,5 @@
import { ADMIN_API_URL, ADMIN_SOCKETS_TOKEN, ALLOW_ARTILLERY, SECRET_KEY } from "../Enum/EnvironmentVariable"; import { ADMIN_SOCKETS_TOKEN, SECRET_KEY } from "../Enum/EnvironmentVariable";
import { uuid } from "uuidv4"; import Jwt from "jsonwebtoken";
import Jwt, { verify } from "jsonwebtoken";
import { TokenInterface } from "../Controller/AuthenticateController";
import { adminApi, AdminBannedData } from "../Services/AdminApi";
export interface AuthTokenData { export interface AuthTokenData {
identifier: string; //will be a email if logged in or an uuid if anonymous identifier: string; //will be a email if logged in or an uuid if anonymous

View File

@ -3,11 +3,28 @@ 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/",
"takeOnFails": true, "takeOnFails": true,
"thumbnails": false, "thumbnails": false,
},
"assertionTimeout": 20000,
"selectorTimeout": 60000,
"videoPath": "screenshots/videos",
"videoOptions": {
"failedOnly": true,
} }
/*"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;
});
` } ]*/
} }

5
tests/Dockerfile Normal file
View File

@ -0,0 +1,5 @@
FROM testcafe/testcafe:1.17.1
USER root
RUN apk add docker-compose
USER user

734
tests/package-lock.json generated
View File

@ -4,7 +4,13 @@
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"dependencies": {
"@ffmpeg-installer/ffmpeg": "^1.1.0",
"@types/dockerode": "^3.3.0",
"axios": "^0.24.0"
},
"devDependencies": { "devDependencies": {
"dockerode": "^3.3.1",
"testcafe": "^1.17.1" "testcafe": "^1.17.1"
} }
}, },
@ -1804,6 +1810,123 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@ffmpeg-installer/darwin-arm64": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/darwin-arm64/-/darwin-arm64-4.1.5.tgz",
"integrity": "sha512-hYqTiP63mXz7wSQfuqfFwfLOfwwFChUedeCVKkBtl/cliaTM7/ePI9bVzfZ2c+dWu3TqCwLDRWNSJ5pqZl8otA==",
"cpu": [
"arm64"
],
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@ffmpeg-installer/darwin-x64": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/darwin-x64/-/darwin-x64-4.1.0.tgz",
"integrity": "sha512-Z4EyG3cIFjdhlY8wI9aLUXuH8nVt7E9SlMVZtWvSPnm2sm37/yC2CwjUzyCQbJbySnef1tQwGG2Sx+uWhd9IAw==",
"cpu": [
"x64"
],
"hasInstallScript": true,
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@ffmpeg-installer/ffmpeg": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/ffmpeg/-/ffmpeg-1.1.0.tgz",
"integrity": "sha512-Uq4rmwkdGxIa9A6Bd/VqqYbT7zqh1GrT5/rFwCwKM70b42W5gIjWeVETq6SdcL0zXqDtY081Ws/iJWhr1+xvQg==",
"optionalDependencies": {
"@ffmpeg-installer/darwin-arm64": "4.1.5",
"@ffmpeg-installer/darwin-x64": "4.1.0",
"@ffmpeg-installer/linux-arm": "4.1.3",
"@ffmpeg-installer/linux-arm64": "4.1.4",
"@ffmpeg-installer/linux-ia32": "4.1.0",
"@ffmpeg-installer/linux-x64": "4.1.0",
"@ffmpeg-installer/win32-ia32": "4.1.0",
"@ffmpeg-installer/win32-x64": "4.1.0"
}
},
"node_modules/@ffmpeg-installer/linux-arm": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-arm/-/linux-arm-4.1.3.tgz",
"integrity": "sha512-NDf5V6l8AfzZ8WzUGZ5mV8O/xMzRag2ETR6+TlGIsMHp81agx51cqpPItXPib/nAZYmo55Bl2L6/WOMI3A5YRg==",
"cpu": [
"arm"
],
"hasInstallScript": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@ffmpeg-installer/linux-arm64": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-arm64/-/linux-arm64-4.1.4.tgz",
"integrity": "sha512-dljEqAOD0oIM6O6DxBW9US/FkvqvQwgJ2lGHOwHDDwu/pX8+V0YsDL1xqHbj1DMX/+nP9rxw7G7gcUvGspSoKg==",
"cpu": [
"arm64"
],
"hasInstallScript": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@ffmpeg-installer/linux-ia32": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-ia32/-/linux-ia32-4.1.0.tgz",
"integrity": "sha512-0LWyFQnPf+Ij9GQGD034hS6A90URNu9HCtQ5cTqo5MxOEc7Rd8gLXrJvn++UmxhU0J5RyRE9KRYstdCVUjkNOQ==",
"cpu": [
"ia32"
],
"hasInstallScript": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@ffmpeg-installer/linux-x64": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-x64/-/linux-x64-4.1.0.tgz",
"integrity": "sha512-Y5BWhGLU/WpQjOArNIgXD3z5mxxdV8c41C+U15nsE5yF8tVcdCGet5zPs5Zy3Ta6bU7haGpIzryutqCGQA/W8A==",
"cpu": [
"x64"
],
"hasInstallScript": true,
"optional": true,
"os": [
"linux"
]
},
"node_modules/@ffmpeg-installer/win32-ia32": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/win32-ia32/-/win32-ia32-4.1.0.tgz",
"integrity": "sha512-FV2D7RlaZv/lrtdhaQ4oETwoFUsUjlUiasiZLDxhEUPdNDWcH1OU9K1xTvqz+OXLdsmYelUDuBS/zkMOTtlUAw==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"win32"
]
},
"node_modules/@ffmpeg-installer/win32-x64": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/win32-x64/-/win32-x64-4.1.0.tgz",
"integrity": "sha512-Drt5u2vzDnIONf4ZEkKtFlbvwj6rI3kxw1Ck9fpudmtgaZIHD4ucsWB2lCZBXRxJgXR+2IMSti+4rtM4C4rXgg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
]
},
"node_modules/@miherlosev/esm": { "node_modules/@miherlosev/esm": {
"version": "3.2.26", "version": "3.2.26",
"resolved": "https://registry.npmjs.org/@miherlosev/esm/-/esm-3.2.26.tgz", "resolved": "https://registry.npmjs.org/@miherlosev/esm/-/esm-3.2.26.tgz",
@ -1848,6 +1971,24 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/@types/docker-modem": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.2.tgz",
"integrity": "sha512-qC7prjoEYR2QEe6SmCVfB1x3rfcQtUr1n4x89+3e0wSTMQ/KYCyf+/RAA9n2tllkkNc6//JMUZePdFRiGIWfaQ==",
"dependencies": {
"@types/node": "*",
"@types/ssh2": "*"
}
},
"node_modules/@types/dockerode": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.0.tgz",
"integrity": "sha512-3Mc0b2gnypJB8Gwmr+8UVPkwjpf4kg1gVxw8lAI4Y/EzpK50LixU1wBSPN9D+xqiw2Ubb02JO8oM0xpwzvi2mg==",
"dependencies": {
"@types/docker-modem": "*",
"@types/node": "*"
}
},
"node_modules/@types/error-stack-parser": { "node_modules/@types/error-stack-parser": {
"version": "1.3.18", "version": "1.3.18",
"resolved": "https://registry.npmjs.org/@types/error-stack-parser/-/error-stack-parser-1.3.18.tgz", "resolved": "https://registry.npmjs.org/@types/error-stack-parser/-/error-stack-parser-1.3.18.tgz",
@ -1885,8 +2026,24 @@
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "12.20.37", "version": "12.20.37",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.37.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.37.tgz",
"integrity": "sha512-i1KGxqcvJaLQali+WuypQnXwcplhtNtjs66eNsZpp2P2FL/trJJxx/VWsM0YCL2iMoIJrbXje48lvIQAQ4p2ZA==", "integrity": "sha512-i1KGxqcvJaLQali+WuypQnXwcplhtNtjs66eNsZpp2P2FL/trJJxx/VWsM0YCL2iMoIJrbXje48lvIQAQ4p2ZA=="
"dev": true },
"node_modules/@types/ssh2": {
"version": "0.5.49",
"resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.49.tgz",
"integrity": "sha512-ffxhQhJqgTzrw8NxHTgkaDtAmAj2qxCyoves7ztpRgqvzbHcZTpTcm+ATWuuCbPQzxnnF4F3SGGTLGEWTZpwqA==",
"dependencies": {
"@types/node": "*",
"@types/ssh2-streams": "*"
}
},
"node_modules/@types/ssh2-streams": {
"version": "0.1.9",
"resolved": "https://registry.npmjs.org/@types/ssh2-streams/-/ssh2-streams-0.1.9.tgz",
"integrity": "sha512-I2J9jKqfmvXLR5GomDiCoHrEJ58hAOmFrekfFqmCFd+A6gaEStvWnPykoWUwld1PNg4G5ag1LwdA+Lz1doRJqg==",
"dependencies": {
"@types/node": "*"
}
}, },
"node_modules/acorn-hammerhead": { "node_modules/acorn-hammerhead": {
"version": "0.5.0", "version": "0.5.0",
@ -2012,6 +2169,15 @@
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true "dev": true
}, },
"node_modules/asn1": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
"dev": true,
"dependencies": {
"safer-buffer": "~2.1.0"
}
},
"node_modules/assertion-error": { "node_modules/assertion-error": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
@ -2048,6 +2214,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",
@ -2153,12 +2327,46 @@
} }
] ]
}, },
"node_modules/bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
"integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
"dev": true,
"dependencies": {
"tweetnacl": "^0.14.3"
}
},
"node_modules/bin-v8-flags-filter": { "node_modules/bin-v8-flags-filter": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/bin-v8-flags-filter/-/bin-v8-flags-filter-1.2.0.tgz", "resolved": "https://registry.npmjs.org/bin-v8-flags-filter/-/bin-v8-flags-filter-1.2.0.tgz",
"integrity": "sha512-g8aeYkY7GhyyKRvQMBsJQZjhm2iCX3dKYvfrMpwVR8IxmUGrkpCBFoKbB9Rh0o3sTLCjU/1tFpZ4C7j3f+D+3g==", "integrity": "sha512-g8aeYkY7GhyyKRvQMBsJQZjhm2iCX3dKYvfrMpwVR8IxmUGrkpCBFoKbB9Rh0o3sTLCjU/1tFpZ4C7j3f+D+3g==",
"dev": true "dev": true
}, },
"node_modules/bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"dev": true,
"dependencies": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
}
},
"node_modules/bl/node_modules/readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/bluebird": { "node_modules/bluebird": {
"version": "3.7.2", "version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
@ -2225,6 +2433,30 @@
"url": "https://opencollective.com/browserslist" "url": "https://opencollective.com/browserslist"
} }
}, },
"node_modules/buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"node_modules/buffer-from": { "node_modules/buffer-from": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@ -2319,6 +2551,12 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"dev": true
},
"node_modules/chrome-remote-interface": { "node_modules/chrome-remote-interface": {
"version": "0.30.1", "version": "0.30.1",
"resolved": "https://registry.npmjs.org/chrome-remote-interface/-/chrome-remote-interface-0.30.1.tgz", "resolved": "https://registry.npmjs.org/chrome-remote-interface/-/chrome-remote-interface-0.30.1.tgz",
@ -2449,6 +2687,20 @@
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"dev": true "dev": true
}, },
"node_modules/cpu-features": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.2.tgz",
"integrity": "sha512-/2yieBqvMcRj8McNzkycjW2v3OIUOibBfd2dLEJ0nWts8NobAxwiyw9phVNS6oDL8x8tz9F7uNVFEVpJncQpeA==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"dependencies": {
"nan": "^2.14.1"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -2649,6 +2901,60 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/docker-modem": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-3.0.3.tgz",
"integrity": "sha512-Tgkn2a+yiNP9FoZgMa/D9Wk+D2Db///0KOyKSYZRJa8w4+DzKyzQMkczKSdR/adQ0x46BOpeNkoyEOKjPhCzjw==",
"dev": true,
"dependencies": {
"debug": "^4.1.1",
"readable-stream": "^3.5.0",
"split-ca": "^1.0.1",
"ssh2": "^1.4.0"
},
"engines": {
"node": ">= 8.0"
}
},
"node_modules/docker-modem/node_modules/readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/dockerode": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-3.3.1.tgz",
"integrity": "sha512-AS2mr8Lp122aa5n6d99HkuTNdRV1wkkhHwBdcnY6V0+28D3DSYwhxAk85/mM9XwD3RMliTxyr63iuvn5ZblFYQ==",
"dev": true,
"dependencies": {
"docker-modem": "^3.0.0",
"tar-fs": "~2.0.1"
},
"engines": {
"node": ">= 8.0"
}
},
"node_modules/dockerode/node_modules/tar-fs": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz",
"integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==",
"dev": true,
"dependencies": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.0.0"
}
},
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.3.906", "version": "1.3.906",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.906.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.906.tgz",
@ -2852,6 +3158,31 @@
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"dev": true
},
"node_modules/fs.realpath": { "node_modules/fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -3100,6 +3431,26 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
]
},
"node_modules/ignore": { "node_modules/ignore": {
"version": "5.1.9", "version": "5.1.9",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz",
@ -3549,6 +3900,12 @@
"mkdirp": "bin/cmd.js" "mkdirp": "bin/cmd.js"
} }
}, },
"node_modules/mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"dev": true
},
"node_modules/moment": { "node_modules/moment": {
"version": "2.29.1", "version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
@ -3582,6 +3939,13 @@
"npm": ">=1.4.0" "npm": ">=1.4.0"
} }
}, },
"node_modules/nan": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz",
"integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==",
"dev": true,
"optional": true
},
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "1.3.4", "version": "1.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-1.3.4.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-1.3.4.tgz",
@ -4281,6 +4645,30 @@
"integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==",
"dev": true "dev": true
}, },
"node_modules/split-ca": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz",
"integrity": "sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY=",
"dev": true
},
"node_modules/ssh2": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.5.0.tgz",
"integrity": "sha512-iUmRkhH9KGeszQwDW7YyyqjsMTf4z+0o48Cp4xOwlY5LjtbIAvyd3fwnsoUZW/hXmTCRA3yt7S/Jb9uVjErVlA==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
"asn1": "^0.2.4",
"bcrypt-pbkdf": "^1.0.2"
},
"engines": {
"node": ">=10.16.0"
},
"optionalDependencies": {
"cpu-features": "0.0.2",
"nan": "^2.15.0"
}
},
"node_modules/stackframe": { "node_modules/stackframe": {
"version": "0.3.1", "version": "0.3.1",
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-0.3.1.tgz", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-0.3.1.tgz",
@ -4355,6 +4743,36 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"dev": true,
"dependencies": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"engines": {
"node": ">=6"
}
},
"node_modules/tar-stream/node_modules/readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"dependencies": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/testcafe": { "node_modules/testcafe": {
"version": "1.17.1", "version": "1.17.1",
"resolved": "https://registry.npmjs.org/testcafe/-/testcafe-1.17.1.tgz", "resolved": "https://registry.npmjs.org/testcafe/-/testcafe-1.17.1.tgz",
@ -4889,6 +5307,12 @@
"node": "*" "node": "*"
} }
}, },
"node_modules/tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
"dev": true
},
"node_modules/type-detect": { "node_modules/type-detect": {
"version": "4.0.8", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",
@ -6330,6 +6754,69 @@
"to-fast-properties": "^2.0.0" "to-fast-properties": "^2.0.0"
} }
}, },
"@ffmpeg-installer/darwin-arm64": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/darwin-arm64/-/darwin-arm64-4.1.5.tgz",
"integrity": "sha512-hYqTiP63mXz7wSQfuqfFwfLOfwwFChUedeCVKkBtl/cliaTM7/ePI9bVzfZ2c+dWu3TqCwLDRWNSJ5pqZl8otA==",
"optional": true
},
"@ffmpeg-installer/darwin-x64": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/darwin-x64/-/darwin-x64-4.1.0.tgz",
"integrity": "sha512-Z4EyG3cIFjdhlY8wI9aLUXuH8nVt7E9SlMVZtWvSPnm2sm37/yC2CwjUzyCQbJbySnef1tQwGG2Sx+uWhd9IAw==",
"optional": true
},
"@ffmpeg-installer/ffmpeg": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/ffmpeg/-/ffmpeg-1.1.0.tgz",
"integrity": "sha512-Uq4rmwkdGxIa9A6Bd/VqqYbT7zqh1GrT5/rFwCwKM70b42W5gIjWeVETq6SdcL0zXqDtY081Ws/iJWhr1+xvQg==",
"requires": {
"@ffmpeg-installer/darwin-arm64": "4.1.5",
"@ffmpeg-installer/darwin-x64": "4.1.0",
"@ffmpeg-installer/linux-arm": "4.1.3",
"@ffmpeg-installer/linux-arm64": "4.1.4",
"@ffmpeg-installer/linux-ia32": "4.1.0",
"@ffmpeg-installer/linux-x64": "4.1.0",
"@ffmpeg-installer/win32-ia32": "4.1.0",
"@ffmpeg-installer/win32-x64": "4.1.0"
}
},
"@ffmpeg-installer/linux-arm": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-arm/-/linux-arm-4.1.3.tgz",
"integrity": "sha512-NDf5V6l8AfzZ8WzUGZ5mV8O/xMzRag2ETR6+TlGIsMHp81agx51cqpPItXPib/nAZYmo55Bl2L6/WOMI3A5YRg==",
"optional": true
},
"@ffmpeg-installer/linux-arm64": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-arm64/-/linux-arm64-4.1.4.tgz",
"integrity": "sha512-dljEqAOD0oIM6O6DxBW9US/FkvqvQwgJ2lGHOwHDDwu/pX8+V0YsDL1xqHbj1DMX/+nP9rxw7G7gcUvGspSoKg==",
"optional": true
},
"@ffmpeg-installer/linux-ia32": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-ia32/-/linux-ia32-4.1.0.tgz",
"integrity": "sha512-0LWyFQnPf+Ij9GQGD034hS6A90URNu9HCtQ5cTqo5MxOEc7Rd8gLXrJvn++UmxhU0J5RyRE9KRYstdCVUjkNOQ==",
"optional": true
},
"@ffmpeg-installer/linux-x64": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/linux-x64/-/linux-x64-4.1.0.tgz",
"integrity": "sha512-Y5BWhGLU/WpQjOArNIgXD3z5mxxdV8c41C+U15nsE5yF8tVcdCGet5zPs5Zy3Ta6bU7haGpIzryutqCGQA/W8A==",
"optional": true
},
"@ffmpeg-installer/win32-ia32": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/win32-ia32/-/win32-ia32-4.1.0.tgz",
"integrity": "sha512-FV2D7RlaZv/lrtdhaQ4oETwoFUsUjlUiasiZLDxhEUPdNDWcH1OU9K1xTvqz+OXLdsmYelUDuBS/zkMOTtlUAw==",
"optional": true
},
"@ffmpeg-installer/win32-x64": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/@ffmpeg-installer/win32-x64/-/win32-x64-4.1.0.tgz",
"integrity": "sha512-Drt5u2vzDnIONf4ZEkKtFlbvwj6rI3kxw1Ck9fpudmtgaZIHD4ucsWB2lCZBXRxJgXR+2IMSti+4rtM4C4rXgg==",
"optional": true
},
"@miherlosev/esm": { "@miherlosev/esm": {
"version": "3.2.26", "version": "3.2.26",
"resolved": "https://registry.npmjs.org/@miherlosev/esm/-/esm-3.2.26.tgz", "resolved": "https://registry.npmjs.org/@miherlosev/esm/-/esm-3.2.26.tgz",
@ -6362,6 +6849,24 @@
"fastq": "^1.6.0" "fastq": "^1.6.0"
} }
}, },
"@types/docker-modem": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.2.tgz",
"integrity": "sha512-qC7prjoEYR2QEe6SmCVfB1x3rfcQtUr1n4x89+3e0wSTMQ/KYCyf+/RAA9n2tllkkNc6//JMUZePdFRiGIWfaQ==",
"requires": {
"@types/node": "*",
"@types/ssh2": "*"
}
},
"@types/dockerode": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-3.3.0.tgz",
"integrity": "sha512-3Mc0b2gnypJB8Gwmr+8UVPkwjpf4kg1gVxw8lAI4Y/EzpK50LixU1wBSPN9D+xqiw2Ubb02JO8oM0xpwzvi2mg==",
"requires": {
"@types/docker-modem": "*",
"@types/node": "*"
}
},
"@types/error-stack-parser": { "@types/error-stack-parser": {
"version": "1.3.18", "version": "1.3.18",
"resolved": "https://registry.npmjs.org/@types/error-stack-parser/-/error-stack-parser-1.3.18.tgz", "resolved": "https://registry.npmjs.org/@types/error-stack-parser/-/error-stack-parser-1.3.18.tgz",
@ -6399,8 +6904,24 @@
"@types/node": { "@types/node": {
"version": "12.20.37", "version": "12.20.37",
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.37.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.20.37.tgz",
"integrity": "sha512-i1KGxqcvJaLQali+WuypQnXwcplhtNtjs66eNsZpp2P2FL/trJJxx/VWsM0YCL2iMoIJrbXje48lvIQAQ4p2ZA==", "integrity": "sha512-i1KGxqcvJaLQali+WuypQnXwcplhtNtjs66eNsZpp2P2FL/trJJxx/VWsM0YCL2iMoIJrbXje48lvIQAQ4p2ZA=="
"dev": true },
"@types/ssh2": {
"version": "0.5.49",
"resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-0.5.49.tgz",
"integrity": "sha512-ffxhQhJqgTzrw8NxHTgkaDtAmAj2qxCyoves7ztpRgqvzbHcZTpTcm+ATWuuCbPQzxnnF4F3SGGTLGEWTZpwqA==",
"requires": {
"@types/node": "*",
"@types/ssh2-streams": "*"
}
},
"@types/ssh2-streams": {
"version": "0.1.9",
"resolved": "https://registry.npmjs.org/@types/ssh2-streams/-/ssh2-streams-0.1.9.tgz",
"integrity": "sha512-I2J9jKqfmvXLR5GomDiCoHrEJ58hAOmFrekfFqmCFd+A6gaEStvWnPykoWUwld1PNg4G5ag1LwdA+Lz1doRJqg==",
"requires": {
"@types/node": "*"
}
}, },
"acorn-hammerhead": { "acorn-hammerhead": {
"version": "0.5.0", "version": "0.5.0",
@ -6498,6 +7019,15 @@
} }
} }
}, },
"asn1": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
"dev": true,
"requires": {
"safer-buffer": "~2.1.0"
}
},
"assertion-error": { "assertion-error": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz",
@ -6522,6 +7052,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",
@ -6600,12 +7138,45 @@
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
"dev": true "dev": true
}, },
"bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
"integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=",
"dev": true,
"requires": {
"tweetnacl": "^0.14.3"
}
},
"bin-v8-flags-filter": { "bin-v8-flags-filter": {
"version": "1.2.0", "version": "1.2.0",
"resolved": "https://registry.npmjs.org/bin-v8-flags-filter/-/bin-v8-flags-filter-1.2.0.tgz", "resolved": "https://registry.npmjs.org/bin-v8-flags-filter/-/bin-v8-flags-filter-1.2.0.tgz",
"integrity": "sha512-g8aeYkY7GhyyKRvQMBsJQZjhm2iCX3dKYvfrMpwVR8IxmUGrkpCBFoKbB9Rh0o3sTLCjU/1tFpZ4C7j3f+D+3g==", "integrity": "sha512-g8aeYkY7GhyyKRvQMBsJQZjhm2iCX3dKYvfrMpwVR8IxmUGrkpCBFoKbB9Rh0o3sTLCjU/1tFpZ4C7j3f+D+3g==",
"dev": true "dev": true
}, },
"bl": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
"integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
"dev": true,
"requires": {
"buffer": "^5.5.0",
"inherits": "^2.0.4",
"readable-stream": "^3.4.0"
},
"dependencies": {
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
}
}
},
"bluebird": { "bluebird": {
"version": "3.7.2", "version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
@ -6659,6 +7230,16 @@
"picocolors": "^1.0.0" "picocolors": "^1.0.0"
} }
}, },
"buffer": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
"integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
"dev": true,
"requires": {
"base64-js": "^1.3.1",
"ieee754": "^1.1.13"
}
},
"buffer-from": { "buffer-from": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@ -6734,6 +7315,12 @@
"integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=",
"dev": true "dev": true
}, },
"chownr": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"dev": true
},
"chrome-remote-interface": { "chrome-remote-interface": {
"version": "0.30.1", "version": "0.30.1",
"resolved": "https://registry.npmjs.org/chrome-remote-interface/-/chrome-remote-interface-0.30.1.tgz", "resolved": "https://registry.npmjs.org/chrome-remote-interface/-/chrome-remote-interface-0.30.1.tgz",
@ -6842,6 +7429,16 @@
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"dev": true "dev": true
}, },
"cpu-features": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.2.tgz",
"integrity": "sha512-/2yieBqvMcRj8McNzkycjW2v3OIUOibBfd2dLEJ0nWts8NobAxwiyw9phVNS6oDL8x8tz9F7uNVFEVpJncQpeA==",
"dev": true,
"optional": true,
"requires": {
"nan": "^2.14.1"
}
},
"cross-spawn": { "cross-spawn": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@ -7000,6 +7597,55 @@
"path-type": "^4.0.0" "path-type": "^4.0.0"
} }
}, },
"docker-modem": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-3.0.3.tgz",
"integrity": "sha512-Tgkn2a+yiNP9FoZgMa/D9Wk+D2Db///0KOyKSYZRJa8w4+DzKyzQMkczKSdR/adQ0x46BOpeNkoyEOKjPhCzjw==",
"dev": true,
"requires": {
"debug": "^4.1.1",
"readable-stream": "^3.5.0",
"split-ca": "^1.0.1",
"ssh2": "^1.4.0"
},
"dependencies": {
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
}
}
},
"dockerode": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/dockerode/-/dockerode-3.3.1.tgz",
"integrity": "sha512-AS2mr8Lp122aa5n6d99HkuTNdRV1wkkhHwBdcnY6V0+28D3DSYwhxAk85/mM9XwD3RMliTxyr63iuvn5ZblFYQ==",
"dev": true,
"requires": {
"docker-modem": "^3.0.0",
"tar-fs": "~2.0.1"
},
"dependencies": {
"tar-fs": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.0.1.tgz",
"integrity": "sha512-6tzWDMeroL87uF/+lin46k+Q+46rAJ0SyPGz7OW7wTgblI273hsBqk2C1j0/xNadNLKDTUL9BukSjB7cwgmlPA==",
"dev": true,
"requires": {
"chownr": "^1.1.1",
"mkdirp-classic": "^0.5.2",
"pump": "^3.0.0",
"tar-stream": "^2.0.0"
}
}
}
},
"electron-to-chromium": { "electron-to-chromium": {
"version": "1.3.906", "version": "1.3.906",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.906.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.906.tgz",
@ -7165,6 +7811,17 @@
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
"dev": true
},
"fs.realpath": { "fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -7357,6 +8014,12 @@
"safer-buffer": ">= 2.1.2 < 3" "safer-buffer": ">= 2.1.2 < 3"
} }
}, },
"ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
"integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
"dev": true
},
"ignore": { "ignore": {
"version": "5.1.9", "version": "5.1.9",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.9.tgz",
@ -7699,6 +8362,12 @@
"minimist": "^1.2.5" "minimist": "^1.2.5"
} }
}, },
"mkdirp-classic": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
"dev": true
},
"moment": { "moment": {
"version": "2.29.1", "version": "2.29.1",
"resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz",
@ -7723,6 +8392,13 @@
"integrity": "sha512-KpMNwdQsYz3O/SBS1qJ/o3sqUJ5wSb8gb0pul8CO0S56b9Y2ALm8zCfsjPXsqGFfoNBkDwZuZIAjhsZI03gYVQ==", "integrity": "sha512-KpMNwdQsYz3O/SBS1qJ/o3sqUJ5wSb8gb0pul8CO0S56b9Y2ALm8zCfsjPXsqGFfoNBkDwZuZIAjhsZI03gYVQ==",
"dev": true "dev": true
}, },
"nan": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz",
"integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==",
"dev": true,
"optional": true
},
"nanoid": { "nanoid": {
"version": "1.3.4", "version": "1.3.4",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-1.3.4.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-1.3.4.tgz",
@ -8257,6 +8933,24 @@
"integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==",
"dev": true "dev": true
}, },
"split-ca": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz",
"integrity": "sha1-bIOv82kvphJW4M0ZfgXp3hV2kaY=",
"dev": true
},
"ssh2": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.5.0.tgz",
"integrity": "sha512-iUmRkhH9KGeszQwDW7YyyqjsMTf4z+0o48Cp4xOwlY5LjtbIAvyd3fwnsoUZW/hXmTCRA3yt7S/Jb9uVjErVlA==",
"dev": true,
"requires": {
"asn1": "^0.2.4",
"bcrypt-pbkdf": "^1.0.2",
"cpu-features": "0.0.2",
"nan": "^2.15.0"
}
},
"stackframe": { "stackframe": {
"version": "0.3.1", "version": "0.3.1",
"resolved": "https://registry.npmjs.org/stackframe/-/stackframe-0.3.1.tgz", "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-0.3.1.tgz",
@ -8316,6 +9010,32 @@
"has-flag": "^3.0.0" "has-flag": "^3.0.0"
} }
}, },
"tar-stream": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
"dev": true,
"requires": {
"bl": "^4.0.3",
"end-of-stream": "^1.4.1",
"fs-constants": "^1.0.0",
"inherits": "^2.0.3",
"readable-stream": "^3.1.1"
},
"dependencies": {
"readable-stream": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
"integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
"dev": true,
"requires": {
"inherits": "^2.0.3",
"string_decoder": "^1.1.1",
"util-deprecate": "^1.0.1"
}
}
}
},
"testcafe": { "testcafe": {
"version": "1.17.1", "version": "1.17.1",
"resolved": "https://registry.npmjs.org/testcafe/-/testcafe-1.17.1.tgz", "resolved": "https://registry.npmjs.org/testcafe/-/testcafe-1.17.1.tgz",
@ -8772,6 +9492,12 @@
"safe-buffer": "^5.0.1" "safe-buffer": "^5.0.1"
} }
}, },
"tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=",
"dev": true
},
"type-detect": { "type-detect": {
"version": "4.0.8", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz",

View File

@ -1,8 +1,14 @@
{ {
"devDependencies": { "devDependencies": {
"dockerode": "^3.3.1",
"testcafe": "^1.17.1" "testcafe": "^1.17.1"
}, },
"scripts": { "scripts": {
"test": "testcafe" "test": "testcafe"
},
"dependencies": {
"@ffmpeg-installer/ffmpeg": "^1.1.0",
"@types/dockerode": "^3.3.0",
"axios": "^0.24.0"
} }
} }

View File

@ -1,13 +1,38 @@
import {assertLogMessage} from "./utils/log"; import {assertLogMessage} from "./utils/log";
const fs = require('fs') const fs = require('fs');
const Docker = require('dockerode');
import { Selector } from 'testcafe'; import { Selector } from 'testcafe';
import {userAlice} from "./utils/roles"; import {login} from "./utils/roles";
import {findContainer, rebootBack, rebootPusher, resetRedis, startContainer, stopContainer} from "./utils/containers";
// Note: we are also testing that we can connect if the URL contains a random query string fixture `Reconnection`
fixture `Variables` .page `http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/mousewheel.json`;
.page `http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/Cache/variables_tmp.json?somerandomparam=1`;
test("Test that connection can succeed even if WorkAdventure starts while pusher is down", async (t: TestController) => {
// Let's stop the pusher
const container = await findContainer('pusher');
await stopContainer(container);
const errorMessage = Selector('.error-div');
await t
.navigateTo('http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/mousewheel.json')
.expect(errorMessage.innerText).contains('Unable to connect to WorkAdventure')
await startContainer(container);
await login(t, 'http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/mousewheel.json');
t.ctx.passed = true;
}).after(async t => {
if (!t.ctx.passed) {
console.log("Test failed. Browser logs:")
console.log(await t.getBrowserConsoleMessages());
}
});
/*
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');
@ -18,11 +43,9 @@ test("Test that variables cache in the back don't prevent setting a variable in
// Let's REPLACE the map by a map that has a new variable // 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) // 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'); 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.openWindow('http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/Cache/variables_tmp.json')
.resizeWindow(960, 800)
await t.resizeWindow(960, 800); .useRole(userAlice);
await t.useRole(userAlice);
//.takeScreenshot('after_switch.png'); //.takeScreenshot('after_switch.png');
// Let's check we successfully manage to save the variable value. // Let's check we successfully manage to save the variable value.
@ -35,3 +58,4 @@ test("Test that variables cache in the back don't prevent setting a variable in
console.log(await t.getBrowserConsoleMessages()); console.log(await t.getBrowserConsoleMessages());
} }
}); });
*/

View File

@ -0,0 +1,85 @@
//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');
const path = require("path");
const fs = require('fs');
/**
* Execute Docker compose, passing the correct host directory (in case this is run from the TestCafe container)
*/
export function dockerCompose(command: string): void {
let param = '';
const projectDir = process.env.PROJECT_DIR;
if (!projectDir && fs.existsSync('/project')) {
// We are probably in the docker-image AND we did not pass PROJECT_DIR env variable
throw new Error('Incorrect docker-compose command used to fire testcafe tests. You need to add a PROJECT_DIR environment variable. Please refer to the CONTRIBUTING.md guide');
}
if (projectDir) {
const dirName = path.basename(projectDir);
param = '--project-name '+dirName+' --project-directory '+projectDir;
}
let stdout = execSync('docker-compose '+param+' '+command, {
cwd: __dirname + '/../../../'
});
}
/**
* Returns a container ID based on the container name.
*/
export async function findContainer(name: string): Promise<Dockerode.ContainerInfo> {
const docker = new Dockerode();
const containers = await docker.listContainers();
const foundContainer = containers.find((container) => container.State === 'running' && container.Names.find((containerName) => containerName.includes(name)));
if (foundContainer === undefined) {
throw new Error('Could not find a running container whose name contains "'+name+'"');
}
return foundContainer;
}
export async function stopContainer(container: Dockerode.ContainerInfo): Promise<void> {
const docker = new Dockerode();
await docker.getContainer(container.Id).stop();
}
export async function startContainer(container: Dockerode.ContainerInfo): Promise<void> {
const docker = new Dockerode();
await docker.getContainer(container.Id).start();
}
export async function rebootBack(): Promise<void> {
dockerCompose('up --force-recreate -d back');
}
export function rebootTraefik(): void {
dockerCompose('up --force-recreate -d reverse-proxy');
}
export async function rebootPusher(): Promise<void> {
dockerCompose('up --force-recreate -d pusher');
}
export async function resetRedis(): Promise<void> {
dockerCompose('stop redis');
dockerCompose('rm -f redis');
dockerCompose('up --force-recreate -d redis');
}
export function stopRedis(): void {
dockerCompose('stop redis');
}
export function startRedis(): void {
dockerCompose('start redis');
}

View File

@ -0,0 +1,31 @@
import axios from "axios";
const fs = require('fs');
const ADMIN_API_TOKEN = process.env.ADMIN_API_TOKEN;
export async function getPusherDump(): Promise<any> {
let url = 'http://pusher.workadventure.localhost/dump?token='+ADMIN_API_TOKEN;
if (fs.existsSync('/project')) {
// We are inside a container. Let's use a direct route
url = 'http://pusher:8080/dump?token='+ADMIN_API_TOKEN;
}
return (await axios({
url,
method: 'get',
})).data;
}
export async function getBackDump(): Promise<any> {
let url = 'http://api.workadventure.localhost/dump?token='+ADMIN_API_TOKEN;
if (fs.existsSync('/project')) {
// We are inside a container. Let's use a direct route
url = 'http://back:8080/dump?token='+ADMIN_API_TOKEN;
}
return (await axios({
url,
method: 'get',
})).data;
}

View File

@ -1,20 +1,15 @@
import { Role } from 'testcafe'; import { Role } from 'testcafe';
export const userAlice = Role('http://play.workadventure.localhost/', async t => { export function login(t: TestController, url: string, userName: string = "Alice", characterNumber: number = 2) {
await t t = t
.typeText('input[name="loginSceneName"]', 'Alice') .navigateTo(url)
.click('button.loginSceneFormSubmit') .typeText('input[name="loginSceneName"]', userName)
.click('button.selectCharacterButtonRight') .click('button.loginSceneFormSubmit');
.click('button.selectCharacterButtonRight')
.click('button.selectCharacterSceneFormSubmit')
.click('button.letsgo');
});
export const userBob = Role('http://play.workadventure.localhost/', async t => { for (let i = 0; i < characterNumber; i++) {
await t t = t.click('button.selectCharacterButtonRight');
.typeText('input[name="loginSceneName"]', 'Bob') }
.click('button.loginSceneFormSubmit')
.click('button.selectCharacterButtonRight') return t.click('button.selectCharacterSceneFormSubmit')
.click('button.selectCharacterSceneFormSubmit')
.click('button.letsgo'); .click('button.letsgo');
}); }

202
tests/tests/variables.ts Normal file
View File

@ -0,0 +1,202 @@
import {assertLogMessage} from "./utils/log";
const fs = require('fs');
const Docker = require('dockerode');
import { Selector } from 'testcafe';
import {login} from "./utils/roles";
import {
findContainer,
rebootBack,
rebootPusher,
resetRedis,
rebootTraefik,
startContainer,
stopContainer, stopRedis, startRedis
} from "./utils/containers";
import {getBackDump, getPusherDump} from "./utils/debug";
// Note: we are also testing that passing a random query parameter does not cause any issue.
fixture `Variables`
.page `http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/shared_variables.json?somerandomparam=1`;
test("Test that variables storage works", async (t: TestController) => {
const variableInput = Selector('#textField');
await resetRedis();
await Promise.all([
rebootBack(),
rebootPusher(),
]);
//const mainWindow = await t.getCurrentWindow();
await login(t, 'http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/shared_variables.json?somerandomparam=1');
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')
.pressKey('tab')
.switchToMainWindow()
//.switchToWindow(mainWindow)
.wait(500)
// reload
.navigateTo('http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/shared_variables.json')
.switchToIframe("#cowebsite-buffer iframe")
.expect(variableInput.value).eql('new value')
//.debug()
.switchToMainWindow()
//.wait(5000)
//.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');
rebootTraefik();
//console.log('Rebooting done');
// Maybe we should:
// 1: stop Traefik
// 2: detect reconnecting screen
// 3: start Traefik again
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/')
.wait(3000);
const backDump = await getBackDump();
//console.log('backDump', backDump);
for (const room of backDump) {
if (room.roomUrl === 'http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/shared_variables.json') {
throw new Error('Room still found in back');
}
}
const pusherDump = await getPusherDump();
//console.log('pusherDump', pusherDump);
await t.expect(pusherDump['http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/shared_variables.json']).eql(undefined);
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()
// Now, let's try to kill / reboot the back
await rebootBack();
await t.navigateTo('http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/shared_variables.json')
.switchToIframe("#cowebsite-buffer iframe")
.expect(variableInput.value).eql('value set while Redis stopped')
.selectText(variableInput)
.pressKey('delete')
.typeText(variableInput, 'value set after back restart')
.pressKey('tab')
.switchToMainWindow()
.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 after back restart')
.switchToMainWindow()
// Now, let's try to kill / reboot the back
await rebootPusher();
await t.navigateTo('http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/shared_variables.json')
.switchToIframe("#cowebsite-buffer iframe")
.expect(variableInput.value).eql('value set after back restart')
.selectText(variableInput)
.pressKey('delete')
.typeText(variableInput, 'value set after pusher restart')
.pressKey('tab')
.switchToMainWindow()
.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 after pusher restart')
.switchToMainWindow()
t.ctx.passed = true;
}).after(async t => {
if (!t.ctx.passed) {
console.log("Test 'Test that variables storage works' failed. Browser logs:")
try {
console.log(await t.getBrowserConsoleMessages());
} catch (e) {
console.error('Error while fetching browser logs (maybe linked to a closed iframe?)', e);
try {
console.log('Logs from main window:');
console.log(await t.switchToMainWindow().getBrowserConsoleMessages());
} catch (e) {
console.error('Unable to retrieve logs', e);
}
}
}
});
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');
//const aliceOnPageTmp = userAliceOnPage('http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/Cache/variables_tmp.json');
await login(t, 'http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/Cache/variables_tmp.json', 'Alice', 2);
//.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')
.resizeWindow(960, 800)
await login(t, 'http://play.workadventure.localhost/_/global/maps.workadventure.localhost/tests/Variables/Cache/variables_tmp.json', 'Bob', 3);
//.takeScreenshot('after_switch.png');
// 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 'Test that variables cache in the back don't prevent setting a variable in case the map changes' failed. Browser logs:")
console.log(await t.getBrowserConsoleMessages());
}
});