add end-to-end UI tests. Resolves #30

* add 2e2 tests

* fix prettier

* add workflow

* prettify

* update vue/cli-service

* update readme
This commit is contained in:
Pavel Rybalko 2021-11-17 11:21:00 +07:00 committed by GitHub
parent 49ee8905e6
commit cbdfc7e08b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 2785 additions and 2681 deletions

View File

@ -3,7 +3,6 @@ module.exports = {
extends: [
"eslint:recommended",
"plugin:vue/recommended",
"plugin:prettier/recommended",
"plugin:security/recommended"
],
parserOptions: {

25
.github/workflows/e2e-test.yaml vendored Normal file
View File

@ -0,0 +1,25 @@
name: End to end test
on: push
jobs:
e2e-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Read .nvmrc
run: echo NVMRC=`cat .nvmrc` >> $GITHUB_ENV
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: ${{ env.NVMRC }}
cache: 'yarn'
- run: yarn install
- name: Install operating system dependencies
run: npx playwright install-deps
- run: npm run test:e2e

View File

@ -7,7 +7,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2-beta
- name: Read .nvmrc
run: echo NVMRC=`cat .nvmrc` >> $GITHUB_ENV
- name: Setup node
uses: actions/setup-node@v2
with:
node-version: ${{ env.NVMRC }}
- name: Get yarn cache directory path
id: yarn-cache-dir-path

2
.gitignore vendored
View File

@ -20,4 +20,4 @@ yarn-error.log*
*.sln
*.sw*
.test-results
test-results

View File

@ -65,11 +65,16 @@ yarn run serve
yarn run build
```
#### Run your tests
#### Run unit tests
```
yarn run test:unit
```
#### Run end-to-end UI tests
```
yarn run test:e2e
```
#### Lints and fixes files
```
yarn run lint

View File

@ -12,7 +12,8 @@
"build": "vue-cli-service build",
"deploy": "gh-pages -b gh-pages -d dist -a",
"lint": "vue-cli-service lint",
"test:unit": "vue-cli-service test:unit"
"test:unit": "vue-cli-service test:unit",
"test:e2e": "playwright test"
},
"dependencies": {
"base64-js": "^1.3.0",
@ -27,12 +28,14 @@
"vue-router": "^3.0.1"
},
"devDependencies": {
"@playwright/test": "^1.16.3",
"@types/fluent-ffmpeg": "^2.1.19",
"@types/jest": "^26.0.24",
"@typescript-eslint/parser": "^4.31.2",
"@vue/cli-plugin-eslint": "^4.5.13",
"@vue/cli-plugin-typescript": "^4.5.13",
"@vue/cli-plugin-unit-jest": "^4.5.13",
"@vue/cli-service": "^3.1.4",
"@vue/cli-plugin-eslint": "^4.5.15",
"@vue/cli-plugin-typescript": "^4.5.15",
"@vue/cli-plugin-unit-jest": "^4.5.15",
"@vue/cli-service": "^4.5.15",
"@vue/test-utils": "^1.0.0-beta.20",
"eslint": "^5.8.0",
"eslint-config-prettier": "^6.11.0",
@ -40,10 +43,12 @@
"eslint-plugin-security": "^1.4.0",
"eslint-plugin-vue": "^6.2.2",
"fast-text-encoding": "^1.0.0",
"fluent-ffmpeg": "^2.1.2",
"gh-pages": "^2.0.1",
"html-webpack-inline-source-plugin": "1.0.0-beta.2",
"html-webpack-plugin": "4.0.0-beta.4",
"jest-junit": "^6.0.1",
"playwright": "^1.16.3",
"ts-jest": "^26.4.4",
"vue-template-compiler": "^2.5.17",
"vuetype": "^0.3.2"
@ -58,7 +63,7 @@
"last 2 versions"
],
"jest-junit": {
"outputDirectory": ".test-results/jest",
"outputDirectory": "test-results/jest",
"oputputName": "results.xml"
}
}

13
playwright.config.ts Normal file
View File

@ -0,0 +1,13 @@
import { PlaywrightTestConfig } from "@playwright/test";
const config: PlaywrightTestConfig = {
webServer: {
command: "NODE_ENV=test npm run serve -- --port 8888",
port: 8888,
reuseExistingServer: false
},
testDir: "tests/e2e",
use: {
browserName: "chromium"
}
};
export default config;

View File

@ -15,10 +15,10 @@
<GeneralInfo />
<div class="measure">
<div v-if="secure">
<router-link class="button-nav" to="/share">
<router-link id="shareNav" class="button-nav" to="/share">
Create
</router-link>
<router-link class="button-nav" to="/combine">
<router-link id="combineNav" class="button-nav" to="/combine">
Restore
</router-link>
</div>

View File

@ -14,6 +14,7 @@
<p>
<label>1. Secret Phrase</label>
<input
id="passphrase"
v-model="passphrase"
type="text"
placeholder="type your passphrase"
@ -23,11 +24,16 @@
</p>
<p>
<label>2. Secret</label>
<textarea v-if="recoveredSecret" v-model="recoveredSecret" readonly />
<textarea
v-if="recoveredSecret"
id="recoveredSecret"
v-model="recoveredSecret"
readonly
/>
<textarea v-else readonly disabled />
</p>
<div v-if="!recoveredSecret">
<button class="button-card" @click="reconstruct">
<button id="reconstructBtn" class="button-card" @click="reconstruct">
Reconstruct Secret
</button>
</div>

View File

@ -7,6 +7,7 @@
<p>
<label>1. Name of your split</label>
<input
id="secretTitle"
v-model="title"
type="text"
:disabled="encryptionMode"
@ -17,6 +18,7 @@
<p>
<label>2. Secret</label>
<textarea
id="secret"
v-model="secret"
:class="{ tooLong: secretTooLong }"
:disabled="encryptionMode"
@ -31,6 +33,7 @@
<br />
Will require any {{ requiredShards }} shards out of
<input
id="totalShards"
v-model.number="totalShards"
:disabled="encryptionMode"
type="number"
@ -40,12 +43,22 @@
to reconstruct
</p>
<button
id="generateBtn"
class="button-card"
:disabled="secretTooLong"
:hidden="encryptionMode"
v-on:click="toggleMode"
>
<span v-if="encryptionMode">Back to editing data</span>
<span v-else>Generate QR codes!</span>
Generate QR codes!
</button>
<button
id="backToEditBtn"
class="button-card"
:disabled="secretTooLong"
:hidden="!encryptionMode"
v-on:click="toggleMode"
>
Back to editing data
</button>
</div>
@ -60,7 +73,7 @@
</div>
</div>
<div class="card" transparent="true">
<button class="button-card" @click="print">
<button id="printBtn" class="button-card" @click="print">
Print us!
</button>
<shard-info
@ -99,7 +112,7 @@ export default Vue.extend({
title: "",
secret: "",
totalShards: 3, // TODO: 5
recoveryPassphrase: passPhrase.generate(4),
recoveryPassphrase: "",
encryptionMode: false
};
},
@ -130,12 +143,19 @@ export default Vue.extend({
return [];
}
},
created: function() {
this.regenPassphrase();
},
mounted: function() {
this.$eventHub.$emit("foldGeneralInfo");
this.$eventHub.$emit("clearAlerts");
},
methods: {
regenPassphrase: function() {
if (process.env.NODE_ENV === "test") {
this.recoveryPassphrase = "TEST";
return;
}
this.recoveryPassphrase = passPhrase.generate(4);
},
print: function() {

51
tests/e2e/index.spec.ts Normal file
View File

@ -0,0 +1,51 @@
import { test, expect } from "@playwright/test";
import { chromium } from "playwright";
import { makeVideoFromImages, scaleImages } from "./util/ffmpeg";
test("happy path", async ({ page, baseURL }, testInfo) => {
const secret = "top secret";
const images = `%01d.png`;
const video = "fake-camera.y4m";
await test.step("create QR codes", async () => {
await page.goto(`${baseURL}`);
await page.click("#shareNav");
await page.fill("#secretTitle", "Title");
await page.fill("#secret", secret);
await page.click("#generateBtn");
const qrCodes = page.locator("#print .qr-tile .card-qr canvas");
await expect(qrCodes).toHaveCount(3);
await qrCodes.first().screenshot({ path: testInfo.outputPath("1.png") });
await qrCodes.last().screenshot({ path: testInfo.outputPath("2.png") });
});
await test.step("make qr code images the same size", async () => {
await scaleImages(testInfo.outputPath(images));
});
await test.step("make video from qr codes", async () => {
await makeVideoFromImages(
testInfo.outputPath(images),
testInfo.outputPath(video)
);
});
await test.step("restore secret", async () => {
const browser = await chromium.launch({
args: [
"--use-fake-ui-for-media-stream",
"--use-fake-device-for-media-stream",
`--use-file-for-fake-video-capture=${testInfo.outputPath(video)}`,
],
});
const page = await browser.newPage();
await page.goto(`${baseURL}`);
await page.click("#combineNav");
await page.waitForSelector("#passphrase");
await page.fill("#passphrase", "TEST");
await page.click("#reconstructBtn");
await expect(page.locator("#recoveredSecret")).toHaveValue(secret);
});
});

27
tests/e2e/util/ffmpeg.ts Normal file
View File

@ -0,0 +1,27 @@
const ffmpeg = require("fluent-ffmpeg");
export function scaleImages(imagesPattern: string) {
return new Promise((resolve, reject) => {
ffmpeg()
.input(imagesPattern)
.outputOptions(["-vf scale=200:200"])
.output(imagesPattern)
.on("end", resolve)
.on("error", reject)
.run();
});
}
export function makeVideoFromImages(imagesPattern: string, videoPath: string) {
return new Promise((resolve, reject) => {
ffmpeg()
.input(imagesPattern)
.inputFPS(24)
.videoFilters("pad=360:240", "format=yuv420p")
.fps(24)
.output(videoPath)
.on("end", resolve)
.on("error", reject)
.run();
});
}

5272
yarn.lock

File diff suppressed because it is too large Load Diff