From f84b31244752051c5b2d93782bb646d86a094724 Mon Sep 17 00:00:00 2001 From: Sebastien DUMETZ Date: Mon, 20 Jan 2025 16:41:16 +0100 Subject: [PATCH] end to end tests prototype --- .github/workflows/build.yml | 31 ++- source/e2e/.gitignore | 6 + source/e2e/__test_fixtures/cube.glb | Bin 0 -> 1936 bytes .../new-article-OKiTjtY6zrbJ-EN.html | 1 + source/e2e/__test_fixtures/scene.svx.json | 1 + source/e2e/__test_fixtures/scene.zip | Bin 0 -> 2558 bytes source/e2e/eCorpus.setup.ts | 94 ++++++++++ source/e2e/package-lock.json | 143 ++++++++++++++ source/e2e/package.json | 21 +++ source/e2e/playwright.config.ts | 56 ++++++ source/e2e/start_server.sh | 18 ++ source/e2e/tests/admin.test.ts | 21 +++ source/e2e/tests/download.spec.ts | 61 ++++++ source/e2e/tests/edition.spec.ts | 80 ++++++++ source/e2e/tests/login.spec.ts | 120 ++++++++++++ source/e2e/tests/upload_object.spec.ts | 74 ++++++++ source/e2e/tests/upload_zip.spec.ts | 176 ++++++++++++++++++ source/e2e/tests/userSettings.spec.ts | 137 ++++++++++++++ source/e2e/tests/view.spec.ts | 61 ++++++ source/voyager | 2 +- 20 files changed, 1101 insertions(+), 2 deletions(-) create mode 100644 source/e2e/.gitignore create mode 100644 source/e2e/__test_fixtures/cube.glb create mode 100644 source/e2e/__test_fixtures/new-article-OKiTjtY6zrbJ-EN.html create mode 100644 source/e2e/__test_fixtures/scene.svx.json create mode 100644 source/e2e/__test_fixtures/scene.zip create mode 100644 source/e2e/eCorpus.setup.ts create mode 100644 source/e2e/package-lock.json create mode 100644 source/e2e/package.json create mode 100644 source/e2e/playwright.config.ts create mode 100755 source/e2e/start_server.sh create mode 100644 source/e2e/tests/admin.test.ts create mode 100644 source/e2e/tests/download.spec.ts create mode 100644 source/e2e/tests/edition.spec.ts create mode 100644 source/e2e/tests/login.spec.ts create mode 100644 source/e2e/tests/upload_object.spec.ts create mode 100644 source/e2e/tests/upload_zip.spec.ts create mode 100644 source/e2e/tests/userSettings.spec.ts create mode 100644 source/e2e/tests/view.spec.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2ffd0501..ffed5923 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -83,4 +83,33 @@ jobs: name: "eCorpus-${{github.ref_name}}" path: eCorpus if-no-files-found: error - retention-days: 10 \ No newline at end of file + retention-days: 10 + e2e: + name: Test End-to-End + runs-on: ubuntu-latest + needs: [ pack ] + steps: + - uses: actions/checkout@v4 + with: + sparse-checkout: | + source/e2e + - uses: actions/download-artifact@v4 + with: + name: "eCorpus-${{github.ref_name}}" + path: build + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: 'npm' + - name: install dependencies + run: | + ls + npm ci + (cd build && npm ci --omit=dev) + (cd source/e2e && npm ci) + (cd source/e2e && npx playwright install --with-deps) + - name: run tests + working-directory: source/e2e + env: + ROOT_DIR: "${{ github.workspace}}/build" + run: npm test diff --git a/source/e2e/.gitignore b/source/e2e/.gitignore new file mode 100644 index 00000000..991f792e --- /dev/null +++ b/source/e2e/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ +/playwright/.auth/ diff --git a/source/e2e/__test_fixtures/cube.glb b/source/e2e/__test_fixtures/cube.glb new file mode 100644 index 0000000000000000000000000000000000000000..190fe2a0aa77c73fb4ae2f6bfbc69a50d075524c GIT binary patch literal 1936 zcmb7EZBH6O5FV?o*0$Q(x36SBU+Hn6!hI-Jswu5<4G`1Bn0mm1mmGIyZ?QFmgrCrV z)c&;2?A`)-8?ru+&QxfEuMAq85v1=*Ej6G2qA% zCpb)Sc(C)9IONjxeXm^)2fed^M;JY1EjQwo&=1AF+g4Q`o7tkNMlM&f%#u|y?qkW( zv62Lu3X2vx*Oe{1IZ`EHZK~+2Zm8<;pu+2W(z{5Uu9U)S4J4^U_RVQjn^mW#x>9rM zhdT$%&Kaz^_r2mc^-n)cW?R^GFX(t}3dvtL3u8vA!EPH}Q7Gc>wFccT7e^ldsYu&l z|2zzMAe)!xct@K#Ys+Bma1h{YunNTX3vViC;D;*q@T!mBbFfoa5AFK+lg(0%YF$cxz=NK|Cu$Xo z1|x&h?W*4m-n?{0kA6b=LTL-HM7QGZSVeZ( z^z>fqBxmfgMQV_b2K8%F##&<27%da*6ebm~`DS9~@uYnEd{aCclaf=Ia;)id%;eFS z89W*C;Y`cFA7Xo25^jvXv?Lta(~@u>ah~D?oLVOw*#RZlwN8E-rzpqhBYm9sl>=>L zC;Yg)0MFnhEZ|&*$M6U?aNU5{@D^Ud8m?>b0-nPXtm3)~Z{Q(3fE8R<;0dh5QzTr+ Hxd{INQjgB- literal 0 HcmV?d00001 diff --git a/source/e2e/__test_fixtures/new-article-OKiTjtY6zrbJ-EN.html b/source/e2e/__test_fixtures/new-article-OKiTjtY6zrbJ-EN.html new file mode 100644 index 00000000..5020ef2e --- /dev/null +++ b/source/e2e/__test_fixtures/new-article-OKiTjtY6zrbJ-EN.html @@ -0,0 +1 @@ +

Article #1


This is the article content

\ No newline at end of file diff --git a/source/e2e/__test_fixtures/scene.svx.json b/source/e2e/__test_fixtures/scene.svx.json new file mode 100644 index 00000000..8727c304 --- /dev/null +++ b/source/e2e/__test_fixtures/scene.svx.json @@ -0,0 +1 @@ +{"asset":{"type":"application/si-dpo-3d.document+json","version":"1.0","copyright":"(c) Holusion SAS, all rights reserved","generator":"Voyager"},"scene":0,"scenes":[{"units":"m","meta":0,"nodes":[0,1,6],"setup":0}],"nodes":[{"id":"00000001","name":"Camera","camera":0},{"id":"00000002","name":"Lights","rotation":[0,0.6112574,0,0.7914319],"scale":[1,1,1],"children":[2,3,4,5]},{"id":"00000003","name":"Key","translation":[-0.8783957,0.9617225,1.6197255],"rotation":[0.4829741,-0.1070728,0.1880998,0.8484633],"scale":[0.3464102,0.3464102,0.3464102],"light":0},{"id":"00000004","name":"Fill #1","translation":[1.5828277,0.9357686,0.9690167],"rotation":[0.3546969,0.163893,-0.3861077,0.8356136],"scale":[0.3464102,0.3464102,0.3464102],"light":1},{"id":"00000005","name":"Fill #2","translation":[-1.2128678,-1.586092,0.5772901],"rotation":[0.9374013,-0.3018693,0.0532277,0.1652891],"scale":[0.3464102,0.3464102,0.3464102],"light":2},{"id":"00000006","name":"Rim","translation":[1.8054166,0.1076425,-1.0241503],"rotation":[0.373256,0.6426073,-0.5786063,0.3360813],"scale":[0.3464102,0.3464102,0.3464102],"light":3},{"id":"ecpfjsP48xwp","name":"Cube","meta":1,"model":0}],"cameras":[{"type":"perspective","perspective":{"yfov":52,"znear":0.0154239,"zfar":15.4238792},"autoNearFar":true}],"lights":[{"color":[1,0.95,0.9],"intensity":1,"type":"directional","shadowEnabled":true,"shadowSize":3.4641016},{"color":[0.9,0.95,1],"intensity":0.7,"type":"directional","shadowEnabled":true,"shadowSize":3.4641016},{"color":[0.8,0.85,1],"intensity":0.5,"type":"directional"},{"color":[0.85,0.9078313,1],"intensity":0.6,"type":"directional"}],"models":[{"units":"m","boundingBox":{"min":[-1,-1,-1],"max":[1,1,1]},"derivatives":[{"usage":"Web3D","quality":"High","assets":[{"uri":"models/cube.glb","type":"Model","byteSize":1936,"numFaces":12,"imageSize":8192}]}],"annotations":[{"id":"7v6L2wf64nmD","titles":{"EN":"Short Annotation"},"leads":{"EN":""},"position":[-0.9999999,0.1811343,0.0137845],"direction":[-0.9999846,-0.0039215,-0.0039215],"scale":0.0866025},{"id":"Of7CMr1ruYX5","titles":{"EN":"Extended Annotation"},"leads":{"EN":"with lead text"},"style":"Extended","position":[-0.0318588,0.1712172,1],"direction":[-0.0039215,-0.0039215,0.9999846],"scale":0.0866025},{"id":"hED2f4NxDFqR","titles":{"EN":"Link annotation"},"leads":{"EN":""},"articleId":"OKiTjtY6zrbJ","style":"Extended","position":[-0.18515,1,0.3567472],"direction":[-0.0039215,0.9999846,-0.0039215],"scale":0.0866025}]}],"metas":[{"collection":{"titles":{"EN":""},"intros":{"EN":""}}},{"collection":{"titles":{"EN":"cube_test","FR":"cube_test"},"intros":{"EN":""}},"articles":[{"id":"OKiTjtY6zrbJ","uris":{"EN":"articles/new-article-OKiTjtY6zrbJ-EN.html"},"titles":{"EN":"First article"},"leads":{"EN":"Lead text of first article"},"taglist":{"EN":["foo"]}}]}],"setups":[{"units":"cm","interface":{"visible":true,"logo":true,"menu":true,"tools":true},"viewer":{"shader":"Default","exposure":1,"gamma":2,"toneMapping":false,"isWallMountAR":false,"arScale":1},"reader":{"enabled":false,"position":"Overlay"},"navigation":{"type":"Orbit","enabled":true,"autoZoom":true,"lightsFollowCamera":true,"autoRotation":false,"orbit":{"orbit":[-25.0705596,-27.9480511,0],"offset":[0,0,3.8023018],"minOrbit":[-90,null,null],"maxOrbit":[90,null,null],"minOffset":[null,null,0.1],"maxOffset":[null,null,10000]}},"background":{"style":"RadialGradient","color0":[0.06,0.19,0.25],"color1":[0.04,0.04,0.04]},"floor":{"visible":false,"position":[0,-1,0],"size":2,"color":[0.6,0.75,0.8],"opacity":0.5,"receiveShadow":false,"autoSize":true},"grid":{"visible":false,"color":[0.5,0.7,0.8]},"tape":{"enabled":false},"slicer":{"enabled":false,"axis":"X","inverted":false,"position":0.5,"color":[0,0.61,0.87]},"language":{"language":"EN"},"environment":{"index":0},"audio":{"narrationId":""},"tours":[{"id":"6SJ7mM49SRTX","titles":{"EN":"New Tour #0"},"leads":{"EN":""},"steps":[{"id":"pwbF2C","titles":{"EN":"New Step #0"}},{"id":"FSMoQ5","titles":{"EN":"New Step #1"}}]}],"snapshots":{"features":["reader","viewer","navigation"],"targets":["scenes/0/setup/reader/enabled","scenes/0/setup/reader/position","scenes/0/setup/reader/articleId","scenes/0/setup/viewer/annotationsVisible","scenes/0/setup/viewer/activeAnnotation","scenes/0/setup/viewer/activeTags","scenes/0/setup/viewer/shader","scenes/0/setup/viewer/exposure","scenes/0/setup/navigation/orbit","scenes/0/setup/navigation/offset"],"states":[{"id":"scene-default","values":[false,0,"",false,"hED2f4NxDFqR","",0,1,[-28.2822384,-44.5417251,0],[0,0,3.8559698]],"curve":"EaseOutQuad","duration":1,"threshold":0},{"id":"pwbF2C","values":[false,0,"",false,"hED2f4NxDFqR","",0,1,[-28.2822384,-44.5417251,0],[0,0,3.8559698]],"curve":"EaseOutQuad","duration":1.5},{"id":"FSMoQ5","values":[false,0,"",true,"hED2f4NxDFqR","",0,1,[-31.4939173,75.3609513,0],[0,0,3.8559698]],"curve":"EaseOutQuad","duration":1.5}]}}]} \ No newline at end of file diff --git a/source/e2e/__test_fixtures/scene.zip b/source/e2e/__test_fixtures/scene.zip new file mode 100644 index 0000000000000000000000000000000000000000..b6fb300ca68b2cc2145bc9ca47d70f42b7c68894 GIT binary patch literal 2558 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 literal 0 HcmV?d00001 diff --git a/source/e2e/eCorpus.setup.ts b/source/e2e/eCorpus.setup.ts new file mode 100644 index 00000000..99ae10e9 --- /dev/null +++ b/source/e2e/eCorpus.setup.ts @@ -0,0 +1,94 @@ +import { test as setup, expect } from '@playwright/test'; +import { randomBytes } from 'node:crypto'; + +const adminFile = 'playwright/.auth/admin.json'; +const userFile = "playwright/.auth/user.json"; +setup.use({ + locale: "en-US", +}); +/** + * To provide reasonable isolation on each test run, this setup uses a "master" admin account to create 2 new randomized + */ +setup("create superAdmin account", async ({request})=>{ + //Expect instance to be in open mode + let res = await request.post("/users", { + data: JSON.stringify({ + username: "testAdmin", + email: "testAdmin@example.com", + password: "12345678", + isAdministrator: true, + }), + headers:{ + "Content-Type": "application/json", + } + }); + + if(res.status() == 401){ + //Happens when setup is run multiple times against the same dev server + //ie. in watch mode + let res = await fetch("http://localhost:8000/users", { + headers: { + "Authorization": `Basic ${Buffer.from(`testAdmin:12345678`).toString("base64")}` + } + }); + expect(res.status).toEqual(200); + }else{ + expect(res.status()).toEqual(201); + } +}); + +setup('authenticate as admin', async ({ request }) => { + //Create administrator + const username = `testAdmin${randomBytes(2).readUInt16LE().toString(36)}`; + const password = randomBytes(16).toString("base64"); + let res = await fetch("http://localhost:8000/users", { + method: "POST", + body: JSON.stringify({ + username, + email: `${username}@example.com`, + password, + isAdministrator: true, + }), + headers:{ + "Content-Type": "application/json", + "Authorization": `Basic ${Buffer.from(`testAdmin:12345678`).toString("base64")}` + } + }); + expect(res.status).toEqual(201); + + let post = await request.post("/auth/login", { + data: JSON.stringify({username, password}), + headers:{ + "Content-Type": "application/json", + } + }); + expect(post.status()).toEqual(200); + await request.storageState({ path: adminFile }); +}); + +setup("authenticate as user", async ({request})=>{ + const username = `testUser${randomBytes(2).readUInt16LE().toString(36)}`; + const password = randomBytes(16).toString("base64"); + let res = await request.post("/users", { + data: JSON.stringify({ + username, + email: `${username}@example.com`, + password, + isAdministrator: false, + }), + headers:{ + "Content-Type": "application/json", + "Authorization": `Basic ${Buffer.from(`testAdmin:12345678`).toString("base64")}` + } + }); + expect(res.status()).toEqual(201); + + res = await request.post("/auth/login", { + data: JSON.stringify({username, password}), + headers:{ + "Content-Type": "application/json", + } + }); + expect(res.status()).toEqual(200); + await request.storageState({ path: userFile }); +}) \ No newline at end of file diff --git a/source/e2e/package-lock.json b/source/e2e/package-lock.json new file mode 100644 index 00000000..2e450be1 --- /dev/null +++ b/source/e2e/package-lock.json @@ -0,0 +1,143 @@ +{ + "name": "e2e", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "e2e", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@types/yauzl": "^2.10.3", + "xml-js": "^1.6.11", + "yauzl": "^3.2.0" + }, + "devDependencies": { + "@playwright/test": "^1.49.1", + "@types/node": "^22.10.7" + } + }, + "node_modules/@playwright/test": { + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.49.1.tgz", + "integrity": "sha512-Ky+BVzPz8pL6PQxHqNRW1k3mIyv933LML7HktS8uik0bUXNCdPhoS/kLihiO1tMf/egaJb4IutXd7UywvXEW+g==", + "dev": true, + "dependencies": { + "playwright": "1.49.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.10.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.7.tgz", + "integrity": "sha512-V09KvXxFiutGp6B7XkpaDXlNadZxrzajcY50EuoLIpQ6WWYCSvf19lVIazzfIzQvhUN2HjX12spLojTnhuKlGg==", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "engines": { + "node": "*" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" + }, + "node_modules/playwright": { + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.49.1.tgz", + "integrity": "sha512-VYL8zLoNTBxVOrJBbDuRgDWa3i+mfQgDTrL8Ah9QXZ7ax4Dsj0MSq5bYgytRnDVVe+njoKnfsYkH3HzqVj5UZA==", + "dev": true, + "dependencies": { + "playwright-core": "1.49.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.49.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.49.1.tgz", + "integrity": "sha512-BzmpVcs4kE2CH15rWfzpjzVGhWERJfmnXmniSyKeRZUs9Ws65m+RGIi7mjJK/euCegfn3i7jvqWeWyHe9y3Vgg==", + "dev": true, + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==" + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + }, + "node_modules/xml-js": { + "version": "1.6.11", + "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", + "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", + "dependencies": { + "sax": "^1.2.4" + }, + "bin": { + "xml-js": "bin/cli.js" + } + }, + "node_modules/yauzl": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.2.0.tgz", + "integrity": "sha512-Ow9nuGZE+qp1u4JIPvg+uCiUr7xGQWdff7JQSk5VGYTAZMDe2q8lxJ10ygv10qmSj031Ty/6FNJpLO4o1Sgc+w==", + "dependencies": { + "buffer-crc32": "~0.2.3", + "pend": "~1.2.0" + }, + "engines": { + "node": ">=12" + } + } + } +} diff --git a/source/e2e/package.json b/source/e2e/package.json new file mode 100644 index 00000000..3afc70ce --- /dev/null +++ b/source/e2e/package.json @@ -0,0 +1,21 @@ +{ + "name": "e2e", + "version": "1.0.0", + "description": "", + "main": "index.js", + "type": "module", + "scripts": { + "test": "playwright test" + }, + "author": "", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.49.1", + "@types/node": "^22.10.7" + }, + "dependencies": { + "@types/yauzl": "^2.10.3", + "xml-js": "^1.6.11", + "yauzl": "^3.2.0" + } +} diff --git a/source/e2e/playwright.config.ts b/source/e2e/playwright.config.ts new file mode 100644 index 00000000..b5cea874 --- /dev/null +++ b/source/e2e/playwright.config.ts @@ -0,0 +1,56 @@ +import { defineConfig, devices } from '@playwright/test'; +import path from 'path'; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './tests', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'line', + use: { + baseURL: 'http://localhost:8000', + trace: 'on-first-retry', + }, + + projects: [ + { + name: "setup", + testDir: ".", + testMatch: /.*\.setup\.ts/ + }, + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + storageState: 'playwright/.auth/admin.json', + }, + dependencies: ['setup'], + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: `${path.resolve(import.meta.dirname, "start_server.sh")}`, + url: 'http://127.0.0.1:8000', + reuseExistingServer: !process.env.CI, + stdout: 'pipe', + stderr: 'pipe', + }, +}); diff --git a/source/e2e/start_server.sh b/source/e2e/start_server.sh new file mode 100755 index 00000000..63bfccb0 --- /dev/null +++ b/source/e2e/start_server.sh @@ -0,0 +1,18 @@ +#!/bin/sh +set -e +#clean up previous runs +rm -rf /tmp/ecorpus-test-server.* + +TMP="$(mktemp -d /tmp/ecorpus-test-server.XXXXX)" + +: "${ROOT_DIR:="$( cd "$( dirname "$0" )/../.." && pwd )"}" +: "${FILES_DIR:="$TMP"}" + +export ROOT_DIR +export FILES_DIR + +( + set -e + cd "$ROOT_DIR" + npm start +) \ No newline at end of file diff --git a/source/e2e/tests/admin.test.ts b/source/e2e/tests/admin.test.ts new file mode 100644 index 00000000..7fce58f3 --- /dev/null +++ b/source/e2e/tests/admin.test.ts @@ -0,0 +1,21 @@ +import path from "node:path"; + + +import { test } from '@playwright/test'; + +const fixtures = path.resolve(import.meta.dirname, "../__test_fixtures"); + +//Authenticated as admin +test.use({ storageState: 'playwright/.auth/admin.json' }); + + + +test.skip("can create a new user", async ({page})=>{ + await page.goto("/ui/admin/users"); + +}); + +test.skip("can delete a non-admin user", async ({page})=>{ + +}); + diff --git a/source/e2e/tests/download.spec.ts b/source/e2e/tests/download.spec.ts new file mode 100644 index 00000000..b3af77a6 --- /dev/null +++ b/source/e2e/tests/download.spec.ts @@ -0,0 +1,61 @@ +import path from "node:path"; +import fs from "node:fs/promises"; +import { createWriteStream } from "node:fs"; +import { randomUUID, createHash } from "node:crypto"; +import {promisify} from "node:util"; + +import {fromBuffer as fromBufferCb} from "yauzl"; +const fromBuffer = promisify(fromBufferCb); + + +import { test, expect } from '@playwright/test'; +import { Writable } from "node:stream"; +import { on, once } from "node:events"; + +const fixtures = path.resolve(import.meta.dirname, "../__test_fixtures"); + +//Authenticated as normal user +test.use({ storageState: 'playwright/.auth/user.json' }); + +test("downloads a scene archive", async ({page, request})=>{ + const name = randomUUID(); + await request.post(`/scenes/${encodeURIComponent(name)}`,{ + data: await fs.readFile(path.join(fixtures, "cube.glb")), + }); + + await page.goto(`/ui/scenes/${encodeURIComponent(name)}`); + //Check if it _looks like_ the actual scene page + await expect(page.getByRole("heading", {name })).toBeVisible(); + const downloadPromise = page.waitForEvent('download'); + await page.getByRole("link", {name: "Download this scene"}).click(); + const download = await downloadPromise; + let rs = await download.createReadStream(); + let b = Buffer.allocUnsafe(4096); + let size = 0; + let ws = new Writable({ + write(chunk:Buffer, encoding, callback) { + if(b.length < size + chunk.length) b = Buffer.concat([b, Buffer.allocUnsafe(Math.max(1024, chunk.length))]); + size += chunk.copy(b, size); + callback(); + }, + }); + rs.pipe(ws, {end: true}); + await once(rs, "end"); + b = b.subarray(0, size); + const zip = await fromBuffer(b); + let entries :any[] = []; + zip.on("entry", (entry)=>{ + entries.push(entry); + }) + await once(zip, "end"); + expect(entries).toHaveLength(2); + expect(entries.map(e=>e.fileName).sort()).toEqual([ + `scenes/${name}/models/${name}.glb`, + `scenes/${name}/scene.svx.json`, + ]); +}); + + +test.skip("download a bunch of scene archives", async ({page})=>{ + +}); \ No newline at end of file diff --git a/source/e2e/tests/edition.spec.ts b/source/e2e/tests/edition.spec.ts new file mode 100644 index 00000000..8a48d9eb --- /dev/null +++ b/source/e2e/tests/edition.spec.ts @@ -0,0 +1,80 @@ +import path from "node:path"; +import fs from "node:fs/promises"; +import { randomUUID } from "node:crypto"; + +import { test, expect, Page } from '@playwright/test'; + +const fixtures = path.resolve(import.meta.dirname, "../__test_fixtures"); + +//Authenticated as user +test.use({ storageState: 'playwright/.auth/user.json' }); +test.describe.configure({ mode: 'serial' }); + +let scenePage: Page; + +const name = randomUUID(); + +test.beforeAll(async ({request, browser})=>{ + //Create a scene in **FRENCH** to make sure the is no DEFAULT_LANGUAGE creep + let res = await request.post(`/scenes/${encodeURIComponent(name)}?language=FR`,{ + data: await fs.readFile(path.join(fixtures, "cube.glb")), + headers: {"Content-Type": "model/gltf-binary"} + }); + await expect(res).toBeOK(); + scenePage = await browser.newPage(); + await scenePage.goto(`/ui/scenes/${name}/edit`); +}); + + +test.afterAll(async () => { + await scenePage.close(); +}); + +test("shows voyager-story in french", async ()=>{ + await expect(scenePage.getByRole("button", {name: "FR"})).toBeVisible(); +}); + +test("can select the model", async()=>{ + await scenePage.getByText('Cube', { exact: true }).click(); +}); + +test("can create an annotation", async ()=>{ + await scenePage.getByRole("button", {name: "Annotations"}).click(); + + await scenePage.getByRole('button', { name: 'Create' }).click(); + const vp = scenePage.viewportSize(); + await scenePage.mouse.click( vp!.width/1.75, vp!.height/1.75); + await expect(scenePage.getByLabel('annotation', { exact: true })).toBeVisible(); + await expect(scenePage.getByLabel('annotation', { exact: true })).toHaveText("New Annotation"); + + //Still in french + await expect(scenePage.getByRole("button", {name: "FR", exact: true})).toBeVisible(); +}); + +test("can create an article", async ()=>{ + await scenePage.getByRole("button", {name: "Articles"}).click(); + + await scenePage.getByRole('button', { name: 'Create' }).click(); + + let mce = scenePage.locator('sv-article-editor').getByRole('application').locator("iframe").contentFrame(); + await expect(mce.getByRole("heading")).toHaveText("Nouvel Article"); + + //Still in french + await expect(scenePage.getByRole("button", {name: "FR", exact: true})).toBeVisible(); + +}); + +test("can switch to english to edit the article", async ()=>{ + await scenePage.locator(".sv-task-view").getByRole("combobox").selectOption("0")// 0 is English + await expect(scenePage.getByRole("button", {name: "EN", exact: true})).toBeVisible(); + let mce = scenePage.locator('sv-article-editor').getByRole('application').locator("iframe").contentFrame(); + await expect(mce.getByRole("heading")).toHaveText("New Article"); +}) + + +test("can save the scene", async ()=>{ + await scenePage.locator(".sv-task-view").getByRole("combobox").selectOption("5")// 5 is French + await scenePage.getByRole('button', { name: 'Sauvegarder' }).click(); + // @fixme catch notification + await expect(scenePage.getByText("Successfully uploaded file")).toBeVisible({timeout: 500}); +}); diff --git a/source/e2e/tests/login.spec.ts b/source/e2e/tests/login.spec.ts new file mode 100644 index 00000000..9368cb59 --- /dev/null +++ b/source/e2e/tests/login.spec.ts @@ -0,0 +1,120 @@ + + + +import path from "node:path"; +import fs, { readFile } from "node:fs/promises"; +import { randomBytes, randomUUID } from "node:crypto"; + +import { test, expect, Page, BrowserContext } from '@playwright/test'; + +const fixtures = path.resolve(import.meta.dirname, "../__test_fixtures"); + +//Runs unauthenticated, in locale "cimode" +test.use({ storageState: { cookies: [], origins: [] }, locale: "cimode" }); + +let username: string; +let password: string; +let userId :number; + +let adminContext :BrowserContext; + +test.beforeAll(async ({browser})=>{ + adminContext = await browser.newContext({storageState: "playwright/.auth/admin.json"}); + + username = `testUserLogin${randomBytes(2).readUInt16LE().toString(36)}`; + password = randomBytes(16).toString("base64"); + + //Create a user for this specific test suite + let res = await adminContext.request.post("/users", { + data: JSON.stringify({ + username, + email: `${username}@example.com`, + password, + isAdministrator: false, + }), + headers:{ + "Content-Type": "application/json", + } + }); + let body = JSON.parse(await res.text()); + expect(body).toHaveProperty("uid"); + userId = body.uid; +}); + +test.afterAll(async ()=>{ + await adminContext.close(); +}); + +["/ui/", "/auth/login"].forEach((path)=>{ + + test(`can login through ${path}`, async ({page})=>{ + await page.goto(path); + + let userSettingsLink = page.getByRole("link", {name: username}); + await expect(userSettingsLink).not.toBeVisible(); + + await page.getByRole("textbox", {name: "labels.username"}).fill(username); + + await page.getByRole("textbox", {name: "labels.password"}).fill(password); + + await page.getByRole("button", {name: "labels.signin"}).click(); + await page.waitForURL("/ui/"); + await expect(userSettingsLink).toBeVisible(); + }); + + test(`can fail to login through ${path} (bad password)`, async ({page})=>{ + await page.goto(path); + + let userSettingsLink = page.getByRole("link", {name: username}); + await expect(userSettingsLink).not.toBeVisible(); + + await page.getByRole("textbox", {name: "labels.username"}).fill(username); + + await page.getByRole("textbox", {name: "labels.password"}).fill("not-password"); + + await page.getByRole("button", {name: "labels.signin"}).click(); + + await expect(page.getByRole("alert")).toHaveText("errors.Bad password"); + }); +}); + +test(`can login from a private page`, async ({page, request})=>{ + // The idea is you're shown a 404 when trying to access a private scene. + // From there you should be able to login and end-up where you initially requested + + // So we create a scene + let sceneName = randomUUID(); + let res = await adminContext.request.post(`/scenes/${sceneName}`, { + data: await readFile(path.join(fixtures, "cube.glb")), + headers:{ + "Content-Type": "model/gltf-binary", + } + }); + await expect(res).toBeOK(); + + // We patch the scene so it is NOT world-readable + res = await adminContext.request.patch(`/scenes/${sceneName}`, { + data: { + permissions: { + "default": "none", + "any": "none", + [username]: "write", + } + } + }); + await expect(res).toBeOK(); + + //Check that we can't access the scene + let pageRes = await page.goto(`/ui/scenes/${sceneName}/edit`); + expect(pageRes).toBeTruthy(); + expect(pageRes!.status()).toEqual(404); + + await page.getByRole("link", {name:"nav.login"}).click(); + await page.waitForURL(/\/auth\/login/); + await page.getByRole("textbox", {name: "labels.username"}).fill(username); + await page.getByRole("textbox", {name: "labels.password"}).fill(password); + await page.getByRole("button", {name: "labels.signin"}).click(); + await page.waitForURL(`/ui/scenes/${sceneName}/edit`); + await expect(page.getByRole("heading", {name: sceneName})).toBeVisible(); + await expect(page.getByText('VScene', {exact: true})).toBeVisible(); +}); diff --git a/source/e2e/tests/upload_object.spec.ts b/source/e2e/tests/upload_object.spec.ts new file mode 100644 index 00000000..7b35a7d4 --- /dev/null +++ b/source/e2e/tests/upload_object.spec.ts @@ -0,0 +1,74 @@ +import path from "node:path"; +import { randomUUID } from "node:crypto"; + +import { test, expect } from '@playwright/test'; + +const fixtures = path.resolve(import.meta.dirname, "../__test_fixtures"); + +//Authenticated as admin +test.use({ storageState: 'playwright/.auth/user.json' }); + +test("uploads and rename a glb", async ({page, request})=>{ + await page.goto("/ui/upload"); + //We are forced to use the rename otherwise we'd have a name collision + const name = randomUUID(); + const f = page.getByRole("form", {name: "create a new scene"}); + await expect(f).toBeVisible(); + await expect(f.getByRole("combobox", {name: "language"})).toHaveValue("en"); + await f.getByRole("button", {name: "select a file"}).setInputFiles(path.join(fixtures, "cube.glb")); + await f.getByRole("textbox", {name: "scene title"}).fill(name) + await f.getByRole("button", {name: "create a scene"}).click(); + + const uploads = page.getByRole("region", {name: "uploads"}); + await expect(uploads).toBeVisible(); + //Don't check for actual progress bar visibility because that could be too quick to register + const link = uploads.getByRole("link", {name: name}); + await link.click(); + await expect(page).toHaveURL(`/ui/scenes/${name}`); + await expect(page.getByRole("heading", {name})).toBeVisible(); + + let res = await request.get(`/scenes/${name}/scene.svx.json`); + await expect(res).toBeOK(); + let doc = JSON.parse((await res.body()).toString()); + expect(doc).toHaveProperty("setups"); + expect(doc.setups).toHaveLength(1); + expect(doc.setups[0]).toHaveProperty("language", {language: "EN"}); + + + res = await request.get(`/scenes/${name}/models/${name}.glb`); + await expect(res).toBeOK(); + expect(res.headers()).toHaveProperty("etag", "W/4diz3Hx67bxWyU9b_iCJD864pVJ6OGYCPh9sU40QyLs"); +}); + +test("uploads and rename a glb (force FR)", async ({page, request})=>{ + await page.goto("/ui/upload"); + //We are forced to use the rename otherwise we'd have a name collision + const name = randomUUID(); + const uploads = page.getByRole("region", {name: "uploads"}); + const f = page.getByRole("form", {name: "create a new scene"}); + await expect(f).toBeVisible(); + await expect(uploads).not.toBeVisible(); + await f.getByRole("combobox", {name: "language"}).selectOption("fr"); + await f.getByRole("button", {name: "select a file"}).setInputFiles(path.join(fixtures, "cube.glb")); + await f.getByRole("textbox", {name: "scene title"}).fill(name) + await f.getByRole("button", {name: "create a scene"}).click(); + + await expect(uploads).toBeVisible(); + //Don't check for actual progress bar visibility because that could be too quick to register + const link = uploads.getByRole("link", {name: name}); + await link.click(); + await expect(page).toHaveURL(`/ui/scenes/${name}`); + await expect(page.getByRole("heading", {name})).toBeVisible(); + + let res = await request.get(`/scenes/${name}/scene.svx.json`); + await expect(res).toBeOK(); + let doc = JSON.parse((await res.body()).toString()); + expect(doc).toHaveProperty("setups"); + expect(doc.setups).toHaveLength(1); + expect(doc.setups[0]).toHaveProperty("language", {language: "FR"}); + + + res = await request.get(`/scenes/${name}/models/${name}.glb`); + await expect(res).toBeOK(); + expect(res.headers()).toHaveProperty("etag", "W/4diz3Hx67bxWyU9b_iCJD864pVJ6OGYCPh9sU40QyLs"); +}); diff --git a/source/e2e/tests/upload_zip.spec.ts b/source/e2e/tests/upload_zip.spec.ts new file mode 100644 index 00000000..f8b6e0b0 --- /dev/null +++ b/source/e2e/tests/upload_zip.spec.ts @@ -0,0 +1,176 @@ +import path from "node:path"; +import fs from "node:fs/promises"; +import { randomUUID } from "node:crypto"; + +import xml from 'xml-js'; + +import { test, expect } from '@playwright/test'; + +const fixtures = path.resolve(import.meta.dirname, "../__test_fixtures"); + +interface ReducedWebDAVProps{ + path:string, + etag?:string, + size?:number, + mime?:string +} + +function reducePropfind(text:string) :ReducedWebDAVProps[]{ + const root = xml.xml2js(text); + expect(root).toHaveProperty("elements"); + const multistatus = root.elements[0]; + expect(multistatus).toHaveProperty("name", "D:multistatus"); + const responses = multistatus.elements; + return responses.map(({elements})=>{ + const href = elements.find(e=>e.name === "D:href"); + expect(href, `find D:href in ${elements.map(p=>p.name)}`).toBeTruthy(); + let item: ReducedWebDAVProps = { + path: new URL(href.elements.find(e=>e.type === "text").text).pathname, + }; + + const propstat = elements.find(e=>e.name === "D:propstat"); + expect(propstat, `find D:propstat in ${elements.map(p=>p.name)}`).toBeTruthy(); + const props = propstat.elements.find(e=>e.name === "D:prop"); + for(const el of props.elements){ + const content = el.elements?.find(e=>e.type ==="text")?.text; + switch(el.name){ + case "D:getetag": + item.etag = content; + break; + case "D:getcontentlength": + item.size = parseInt(content); + break; + case "D:getcontenttype": + item.mime = content; + break; + } + } + return item; + }); +} + + + +//Authenticated as admin +test.use({ storageState: 'playwright/.auth/admin.json' }); + +test("uploads a scene zip", async ({page, request})=>{ + //Generate the zip file's scene + const name = randomUUID(); + let res = await request.post(`/scenes/${name}`,{ + data: await fs.readFile(path.join(fixtures, "cube.glb")), + }); + await expect(res).toBeOK(); + res = await request.get(`/scenes/${name}`, { + headers: { + "Accept": "application/zip", + } + }); + + let body = await res.body(); + + //Delete the scene + res = await request.delete(`/scenes/${name}?archive=false`); + await expect(res).toBeOK(); + + res = await request.get(`/scenes/${name}`); + await expect(res).not.toBeOK(); + + await page.goto("/ui/upload"); + + const f = page.getByRole("form", {name: "create a new scene"}); + await expect(f).toBeVisible(); + await f.getByRole("button", {name: "select a file"}).setInputFiles({ + name: "scene.zip", + mimeType: "application/zip", + buffer: body, + }); + await f.getByRole("button", {name: "create a scene"}).click(); + + + const uploads = page.getByRole("region", {name: "uploads"}); + await expect(uploads).toBeVisible(); + //Don't check for actual progress bar visibility because that could be too quick to register + const link = uploads.getByRole("link", {name: name}); + await link.click(); + await expect(page).toHaveURL(`/ui/scenes/${name}`); + await expect(page.getByRole("heading", {name})).toBeVisible(); +}); + + +test("uploads a multi-scene zip", async ({page, request})=>{ + //Create scenes for this test + const names = [randomUUID(), randomUUID()]; + await Promise.all(names.map(async (name)=>{ + let res = await request.post(`/scenes/${name}`,{ + data: await fs.readFile(path.join(fixtures, "cube.glb")), + }); + await expect(res).toBeOK(); + })); + + //We rely on this "filter by name" in other softwares anyway so it's a good check + let res = await request.get(`/scenes?${names.map(n=>`name=${n}`).join("&")}`, { + headers: { + "Accept": "application/zip", + } + }); + + //We will compare initial and restored state using the PROPFIND route's metadata + let props :ReducedWebDAVProps[] = (await Promise.all(names.map(async name=>{ + let res = await request.fetch(`/scenes/${name}`, { + method: "PROPFIND", + }); + await expect(res).toBeOK(); + return reducePropfind(await res.text()); + }))).flat(); + + + let body = await res.body(); + + await Promise.all(names.map(async (name)=>{ + //Delete the scene + res = await request.delete(`/scenes/${name}?archive=false`); + await expect(res).toBeOK(); + })); + + + + await page.goto("/ui/upload"); + + const f = page.getByRole("form", {name: "create a new scene"}); + await expect(f).toBeVisible(); + await f.getByRole("button", {name: "select a file"}).setInputFiles({ + name: "scene.zip", + mimeType: "application/zip", + buffer: body, + }); + await f.getByRole("button", {name: "create a scene"}).click(); + + + const uploads = page.getByRole("region", {name: "uploads"}); + for (let name of names){ + await expect(uploads).toBeVisible(); + //Don't check for actual progress bar visibility because that could be too quick to register + const link = uploads.getByRole("link", {name: name}); + } + + await Promise.all(names.map(async (name)=>{ + //Check the scene exists again + let res = await request.get(`/scenes/${name}`); + await expect(res).toBeOK(); + })); + + let updatedProps :ReducedWebDAVProps[] = (await Promise.all(names.map(async name=>{ + let res = await request.fetch(`/scenes/${name}`, { + method: "PROPFIND", + }); + await expect(res).toBeOK(); + return reducePropfind(await res.text()); + }))).flat(); + + expect( + updatedProps.sort((a,b)=>a.path < b.path?-1:1) + ).toEqual( + props.sort((a,b)=>a.path < b.path?-1:1) + ); +}); \ No newline at end of file diff --git a/source/e2e/tests/userSettings.spec.ts b/source/e2e/tests/userSettings.spec.ts new file mode 100644 index 00000000..cd4db211 --- /dev/null +++ b/source/e2e/tests/userSettings.spec.ts @@ -0,0 +1,137 @@ +import path from "node:path"; +import fs, { readFile } from "node:fs/promises"; +import { randomBytes, randomUUID } from "node:crypto"; + +import { test as _test, expect, Page, BrowserContext, APIRequestContext } from '@playwright/test'; + +const fixtures = path.resolve(import.meta.dirname, "../__test_fixtures"); + +type Account = {username: string, password: string, uid: number}; + + +const test = _test.extend<{account:Account}>({ + account: async ({browser}, use)=>{ + let username = `testUserLogin${randomBytes(2).readUInt16LE().toString(36)}`; + let password = randomBytes(16).toString("base64"); + let adminContext = await browser.newContext({storageState: "playwright/.auth/admin.json"}); + //Create a user for this specific test + let res = await adminContext.request.post("/users", { + data: JSON.stringify({ + username, + email: `${username}@example.com`, + password, + isAdministrator: false, + }), + headers:{ + "Content-Type": "application/json", + } + }); + let body = JSON.parse(await res.text()); + expect(body).toHaveProperty("uid"); + let uid :number =body.uid; + await use({username, password, uid}); + await adminContext.close(); + }, + page: async({page, account:{username, password}}, use)=>{ + let res = await page.request.post("/auth/login", { + data: JSON.stringify({username, password}), + headers:{ + "Content-Type": "application/json", + } + }); + expect(res.status()).toEqual(200); + await use(page); + }, +}) + + + + + +//Runs with a per-test storageState, in locale "cimode" +test.use({ storageState: { cookies: [], origins: [] }, locale: "cimode" }); + + +test("can read user settings page", async function({page}){ + await page.goto("/ui/"); + await page.getByRole("link", {"name": "testUserLogin"}).click(); + await page.waitForURL("/ui/user/"); + await expect(page.getByRole("heading", {name: "titles.userSettings"})).toBeVisible(); +}); + + +test("can change email", async ({page, account})=>{ + //Ensure this is unique, otherwise it is rejected + let new_email = `${account.username}-replacement@example2.com` + await page.goto("/ui/user/"); + const form = page.getByRole("form", {name: "titles.userProfile"}); + await expect(form).toBeVisible(); + const emailField = form.getByRole("textbox", {name:"labels.email"}); + await expect(emailField).not.toHaveValue(new_email); + await emailField.fill(new_email); + await form.getByRole("button", {name: "labels.save"}).click(); + + await expect(page.getByRole("progressbar")).not.toBeVisible(); + await expect(page.getByRole("alert")).not.toBeVisible(); + await expect(page.getByRole("status")).toBeVisible(); + + await expect(emailField).toHaveValue(new_email); + await page.reload(); + await expect(emailField).toHaveValue(new_email); +}); + +test("can change password", async ({baseURL, page, account:{username, password}})=>{ + const new_password = randomBytes(10).toString("base64"); + + let res = await fetch(new URL(`/auth/login`, baseURL), { + method: "GET", + headers: { + "Authorization": `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}` + } + }) + expect(res.ok).toBeTruthy(); + + await page.goto("/ui/user/"); + const form = page.getByRole("form", {name: "titles.changePassword"}); + await expect(form).toBeVisible(); + await form.getByRole("textbox", {name: "labels.newPassword"}).fill(new_password); + await form.getByRole("button", {name: "labels.save"}).click(); + + await expect(page.getByRole("progressbar")).not.toBeVisible(); + await expect(page.getByRole("alert")).not.toBeVisible(); + await expect(page.getByRole("status")).toBeVisible(); + + res = await fetch(new URL(`/auth/login`, baseURL), { + method: "GET", + headers: { + "Authorization": `Basic ${Buffer.from(`${username}:${password}`).toString("base64")}` + } + }) + expect(res.ok).toBeFalsy(); + + res = await fetch(new URL(`/auth/login`, baseURL), { + method: "GET", + headers: { + "Authorization": `Basic ${Buffer.from(`${username}:${new_password}`).toString("base64")}` + } + }) + expect(res.ok).toBeTruthy(); + +}); + + +test("can logout", async ({page})=>{ + + let res = await page.request.get(`/auth/login`); + expect(res).toBeOK(); + expect(await res.json()).toHaveProperty("isDefaultUser", false); + + await page.goto("/ui/user/"); + await page.getByRole("button", {name: "buttons.logout"}).click(); + await page.waitForURL("/ui/"); + await expect(page.getByRole("link",{name: "testUserLogin"})).not.toBeVisible(); + + res = await page.request.get(`/auth/login`); + expect(res).toBeOK(); + expect(await res.json()).toHaveProperty("isDefaultUser", true); +}); \ No newline at end of file diff --git a/source/e2e/tests/view.spec.ts b/source/e2e/tests/view.spec.ts new file mode 100644 index 00000000..0cc4e5e6 --- /dev/null +++ b/source/e2e/tests/view.spec.ts @@ -0,0 +1,61 @@ +import path from "node:path"; +import fs from "node:fs/promises"; +import { randomUUID } from "node:crypto"; + +import { test, expect, Page } from '@playwright/test'; + +const fixtures = path.resolve(import.meta.dirname, "../__test_fixtures"); + +//Authenticated as user +test.use({ storageState: 'playwright/.auth/user.json' }); +test.describe.configure({ mode: 'serial' }); + +let scenePage: Page; + +const name = randomUUID(); + +test.beforeAll(async ({request, browser})=>{ + let res = await request.fetch(`/scenes/${encodeURIComponent(name)}`, { + method: "MKCOL" + }); + await expect(res).toBeOK(); + + + + res = await request.put(`/scenes/${encodeURIComponent(name)}/models/cube.glb`,{ + data: await fs.readFile(path.join(fixtures, "cube.glb")), + headers: {"Content-Type": "model/gltf-binary"} + }); + await expect(res).toBeOK(); + + res = await request.put(`/scenes/${encodeURIComponent(name)}/scene.svx.json`, { + data: await fs.readFile(path.join(fixtures, "scene.svx.json")), + headers: {"Content-Type": "application/json"} + }); + await expect(res).toBeOK(); + + res = await request.put(`/scenes/${encodeURIComponent(name)}/articles/new-article-OKiTjtY6zrbJ-EN.html`, { + data: await fs.readFile(path.join(fixtures, "new-article-OKiTjtY6zrbJ-EN.html")), + headers: {"Content-Type": "text/plain"} + }); + await expect(res).toBeOK(); + + scenePage = await browser.newPage(); + await scenePage.goto(`/ui/scenes/${name}/view?prompt=false`); +}); + + +test.afterAll(async () => { + await scenePage.close(); +}); + +test.skip("can show annotations", async ()=>{ + let short = scenePage.getByRole("button", {name:"annotation"}).getByText("Short Annotation"); + await expect(short).not.toBeVisible(); + await scenePage.getByTitle("Show/Hide Annotations").click(); + await expect(short).toBeVisible(); + + let link = scenePage.getByRole("button", {name:"annotation"}).getByText("Link Annotation"); + await link.click(); + await link.getByRole('button', { name: 'Read more...' }).click(); +}); \ No newline at end of file diff --git a/source/voyager b/source/voyager index 3b71b84f..56c7f2ef 160000 --- a/source/voyager +++ b/source/voyager @@ -1 +1 @@ -Subproject commit 3b71b84f7a02709467e4b4f9e7dbb46981e83427 +Subproject commit 56c7f2efaf2cdae27d83d9d5daef1c7baac27a93