diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index f6197365..be668b39 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -27,8 +27,8 @@ jobs: MEDIA_ROOT: /tmp/files_storage CELERY_BROKER_URL: redis://redis:6379/0 CELERY_RESULT_BACKEND: django-db - CELERY_TASK_ALWAYS_EAGER: "false" - CELERY_TASK_EAGER_PROPAGATES: "false" + CELERY_TASK_ALWAYS_EAGER: "true" + CELERY_TASK_EAGER_PROPAGATES: "true" DJANGO_ALLOWED_HOSTS: "localhost 127.0.0.1" DJANGO_TRUSTED_ORIGINS: "http://localhost:3000 http://127.0.0.1:3000 http://localhost:8000 http://127.0.0.1:8000" POSTGRES_USER: postgres @@ -169,7 +169,7 @@ jobs: VIRTUAL_ENV: backend/.dev/venv - name: Run Playwright tests - run: npx playwright test --workers="$(node -p "Math.max(1, Math.min(Math.floor((require('os').cpus()?.length||2)*0.75), 8))")" + run: npx playwright test working-directory: ./e2e env: DJANGO_DB: postgresql diff --git a/backend/Makefile b/backend/Makefile index 08ab1ae0..bae726c4 100644 --- a/backend/Makefile +++ b/backend/Makefile @@ -22,7 +22,7 @@ install: venv install-macos: venv find . -name 'requirements.txt' -exec $(PIP) install -r {} \; $(PIP) install -r requirements.txt - wget -O /tmp/ifcopenshell_python.zip "https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.8.5-1c5b825-macos64.zip" + wget -O /tmp/ifcopenshell_python.zip "https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.8.5-a51b2c5-macos64.zip" mkdir -p $(VIRTUAL_ENV)/lib/python3.11/site-packages unzip /tmp/ifcopenshell_python.zip -d $(VIRTUAL_ENV)/lib/python3.11/site-packages rm /tmp/ifcopenshell_python.zip @@ -30,7 +30,7 @@ install-macos: venv install-macos-m1: venv find . -name 'requirements.txt' -exec $(PIP) install -r {} \; $(PIP) install -r requirements.txt - wget -O /tmp/ifcopenshell_python.zip "https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.8.5-1c5b825-macosm164.zip" + wget -O /tmp/ifcopenshell_python.zip "https://s3.amazonaws.com/ifcopenshell-builds/ifcopenshell-python-311-v0.8.5-a51b2c5-macosm164.zip" mkdir -p $(VIRTUAL_ENV)/lib/python3.11/site-packages unzip /tmp/ifcopenshell_python.zip -d $(VIRTUAL_ENV)/lib/python3.11/site-packages rm /tmp/ifcopenshell_python.zip diff --git a/e2e/fixtures/allowlisting/allowlisted_proxyqto.ifc b/e2e/fixtures/allowlisting/allowlisted_proxyqto.ifc new file mode 100644 index 00000000..9b57d432 --- /dev/null +++ b/e2e/fixtures/allowlisting/allowlisted_proxyqto.ifc @@ -0,0 +1,57 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1'); +FILE_NAME('','2026-02-24T09:56:49',(''),(''),'IfcOpenShell 0.8.0','IfcOpenShell 0.8.0',''); +FILE_SCHEMA(('IFC4')); +ENDSEC; +DATA; +#1=IFCPERSON($,'User','Generated',$,$,$,$,$); +#2=IFCORGANIZATION($,'Example Org',$,$,$); +#3=IFCPERSONANDORGANIZATION(#1,#2,$); +#4=IFCAPPLICATION(#2,'1.0','IfcOpenShell','ifcopenshell'); +#5=IFCOWNERHISTORY(#3,#4,$,.NOTDEFINED.,$,$,$,1771923409); +#6=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.); +#7=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.); +#8=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.); +#9=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.); +#10=IFCUNITASSIGNMENT((#6,#7,#8,#9)); +#11=IFCCARTESIANPOINT((0.,0.,0.)); +#12=IFCDIRECTION((0.,0.,1.)); +#13=IFCDIRECTION((1.,0.,0.)); +#14=IFCAXIS2PLACEMENT3D(#11,#12,#13); +#15=IFCGEOMETRICREPRESENTATIONCONTEXT('Model','Model',3,1.E-05,#14,$); +#16=IFCPROJECT('1BMjysx0n9MvGMpzkFND_A',#5,'Example Project',$,$,$,$,(#15),#10); +#17=IFCCARTESIANPOINT((0.,0.,0.)); +#18=IFCDIRECTION((0.,0.,1.)); +#19=IFCDIRECTION((1.,0.,0.)); +#20=IFCAXIS2PLACEMENT3D(#17,#18,#19); +#21=IFCLOCALPLACEMENT($,#20); +#22=IFCSITE('1Fkx8DCKTFwvnU9ad7fr64',#5,'Site',$,$,#21,$,$,.ELEMENT.,$,$,$,$,$); +#23=IFCCARTESIANPOINT((0.,0.,0.)); +#24=IFCDIRECTION((0.,0.,1.)); +#25=IFCDIRECTION((1.,0.,0.)); +#26=IFCAXIS2PLACEMENT3D(#23,#24,#25); +#27=IFCLOCALPLACEMENT(#21,#26); +#28=IFCBUILDING('3MvFJc7E1C_PJg__ZCQSxq',#5,'Building',$,$,#27,$,$,.ELEMENT.,$,$,$); +#29=IFCCARTESIANPOINT((0.,0.,0.)); +#30=IFCDIRECTION((0.,0.,1.)); +#31=IFCDIRECTION((1.,0.,0.)); +#32=IFCAXIS2PLACEMENT3D(#29,#30,#31); +#33=IFCLOCALPLACEMENT(#27,#32); +#34=IFCBUILDINGSTOREY('283AuHtNn9XA4199h9ReZ0',#5,'Level 0',$,$,#33,$,$,.ELEMENT.,0.); +#35=IFCRELAGGREGATES('36lNwFyi5BIfxze4QqIrl3',#5,$,$,#16,(#22)); +#36=IFCRELAGGREGATES('25bFosZlv5R8R32MNvf9$Y',#5,$,$,#22,(#28)); +#37=IFCRELAGGREGATES('3Mf03PrbD6CfEDjAk6NWrg',#5,$,$,#28,(#34)); +#38=IFCCARTESIANPOINT((0.,0.,0.)); +#39=IFCDIRECTION((0.,0.,1.)); +#40=IFCDIRECTION((1.,0.,0.)); +#41=IFCAXIS2PLACEMENT3D(#38,#39,#40); +#42=IFCLOCALPLACEMENT(#33,#41); +#43=IFCBUILDINGELEMENTPROXY('3FQgpTOKHC6PgbGLrO7C3X',#5,'Proxy 01',$,$,#42,$,$,.NOTDEFINED.); +#44=IFCRELCONTAINEDINSPATIALSTRUCTURE('0jsrh1zyP8hRK5zq7BW7Jz',#5,$,$,(#43),#34); +#45=IFCQUANTITYAREA('NetSurfaceArea',$,$,12.34,$); +#46=IFCQUANTITYVOLUME('NetVolume',$,$,56.78,$); +#47=IFCELEMENTQUANTITY('2ETxYtAXDDVBdYAZjPNhxl',$,'Qto_BuildingElementProxyQuantities',$,'BaseQuantities',(#45,#46)); +#48=IFCRELDEFINESBYPROPERTIES('1RiLQ$p2jD2BR9e6LxFE2r',#5,$,$,(#43),#47); +ENDSEC; +END-ISO-10303-21; diff --git a/e2e/fixtures/allowlisting/allowlisted_storeyqto.ifc b/e2e/fixtures/allowlisting/allowlisted_storeyqto.ifc new file mode 100644 index 00000000..9d8910b0 --- /dev/null +++ b/e2e/fixtures/allowlisting/allowlisted_storeyqto.ifc @@ -0,0 +1,49 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1'); +FILE_NAME('','2026-02-24T09:56:49',(''),(''),'IfcOpenShell 0.8.0','IfcOpenShell 0.8.0',''); +FILE_SCHEMA(('IFC4')); +ENDSEC; +DATA; +#1=IFCPERSON($,'User','Generated',$,$,$,$,$); +#2=IFCORGANIZATION($,'Example Org',$,$,$); +#3=IFCPERSONANDORGANIZATION(#1,#2,$); +#4=IFCAPPLICATION(#2,'1.0','IfcOpenShell','ifcopenshell'); +#5=IFCOWNERHISTORY(#3,#4,$,.NOTDEFINED.,$,$,$,1771923409); +#6=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.); +#7=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.); +#8=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.); +#9=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.); +#10=IFCUNITASSIGNMENT((#6,#7,#8,#9)); +#11=IFCCARTESIANPOINT((0.,0.,0.)); +#12=IFCDIRECTION((0.,0.,1.)); +#13=IFCDIRECTION((1.,0.,0.)); +#14=IFCAXIS2PLACEMENT3D(#11,#12,#13); +#15=IFCGEOMETRICREPRESENTATIONCONTEXT('Model','Model',3,1.E-05,#14,$); +#16=IFCPROJECT('1iyfGnLr1A5QObe35JynRA',#5,'Example Project',$,$,$,$,(#15),#10); +#17=IFCCARTESIANPOINT((0.,0.,0.)); +#18=IFCDIRECTION((0.,0.,1.)); +#19=IFCDIRECTION((1.,0.,0.)); +#20=IFCAXIS2PLACEMENT3D(#17,#18,#19); +#21=IFCLOCALPLACEMENT($,#20); +#22=IFCSITE('0xq5s0Sef4eBW2L6xclQoY',#5,'Site',$,$,#21,$,$,.ELEMENT.,$,$,$,$,$); +#23=IFCCARTESIANPOINT((0.,0.,0.)); +#24=IFCDIRECTION((0.,0.,1.)); +#25=IFCDIRECTION((1.,0.,0.)); +#26=IFCAXIS2PLACEMENT3D(#23,#24,#25); +#27=IFCLOCALPLACEMENT(#21,#26); +#28=IFCBUILDING('19vP5ojgzA4Ak8tpTDmsFk',#5,'Building',$,$,#27,$,$,.ELEMENT.,$,$,$); +#29=IFCCARTESIANPOINT((0.,0.,0.)); +#30=IFCDIRECTION((0.,0.,1.)); +#31=IFCDIRECTION((1.,0.,0.)); +#32=IFCAXIS2PLACEMENT3D(#29,#30,#31); +#33=IFCLOCALPLACEMENT(#27,#32); +#34=IFCBUILDINGSTOREY('0udBjQTrz6qesrXheLTe9j',#5,'Level 0',$,$,#33,$,$,.ELEMENT.,0.); +#35=IFCRELAGGREGATES('168q3RcXXBpw6F0BU2RPbH',#5,$,$,#16,(#22)); +#36=IFCRELAGGREGATES('0nFUVKgJ11avoRp_pS0krY',#5,$,$,#22,(#28)); +#37=IFCRELAGGREGATES('3XdgFhFOPFaf0jfpU9_z85',#5,$,$,#28,(#34)); +#38=IFCQUANTITYLENGTH('NetHeight',$,$,12.34,$); +#39=IFCELEMENTQUANTITY('3KXvj87n5EZR22_nDKi31k',$,'Qto_BuildingStoreyBaseQuantities',$,'BaseQuantities',(#38)); +#40=IFCRELDEFINESBYPROPERTIES('1ihyW38pfDy8xBvjsE5Dq8',#5,$,$,(#34),#39); +ENDSEC; +END-ISO-10303-21; diff --git a/e2e/fixtures/transformer_type_whitelisted.ifc b/e2e/fixtures/allowlisting/allowlisted_transformertype.ifc similarity index 72% rename from e2e/fixtures/transformer_type_whitelisted.ifc rename to e2e/fixtures/allowlisting/allowlisted_transformertype.ifc index 1a8d4cb2..93cc08d2 100644 --- a/e2e/fixtures/transformer_type_whitelisted.ifc +++ b/e2e/fixtures/allowlisting/allowlisted_transformertype.ifc @@ -1,14 +1,14 @@ ISO-10303-21; HEADER; FILE_DESCRIPTION(('ViewDefinition [ReferenceView_V1.2]'),'2;1'); -FILE_NAME('','2026-01-30T19:23:32',(''),(''),'IfcOpenShell contributors - IfcOpenShell - v0.8.0','IfcOpenShell contributors - IfcOpenShell - v0.8.0',''); +FILE_NAME('','2026-01-30T19:23:32',(''),(''),'Test3 - Test3 - v3.0','Test3 - Test3 - v3.0',''); FILE_SCHEMA(('IFC4')); ENDSEC; DATA; #1=IFCPERSON($,$,'',$,$,$,$,$); #2=IFCORGANIZATION($,'',$,$,$); #3=IFCPERSONANDORGANIZATION(#1,#2,$); -#4=IFCAPPLICATION(#2,'0.8.0','IfcOpenShell contributors - IfcOpenShell - v0.8.0',''); +#4=IFCAPPLICATION(#2,'0.8.0','Test3 - Test3 - v3.0',''); #5=IFCOWNERHISTORY(#3,#4,$,.NOTDEFINED.,$,#3,#4,1769801012); #6=IFCDIRECTION((1.,0.,0.)); #7=IFCDIRECTION((0.,0.,1.)); @@ -28,5 +28,9 @@ DATA; #21=IFCTRANSFORMER('152EkAn$n33hZ6u_M0ABgY',$,$,$,$,$,$,$,$); #22=IFCTRANSFORMERTYPE('1_GJgCelfA8uThkk8mMvX9',$,'T',$,$,$,$,$,$,.CURRENT.); #23=IFCRELDEFINESBYTYPE('0_sGsYHQLAMgWs1UcXRuBz',$,$,$,(#21),#22); +#25=IFCRELAGGREGATES('1_FiI$EtrAVumQxKPOEZu6',#5,$,$,#20,(#41)); +#41=IFCSITE('060XCG67v9ZO7i9CQZAKvK',#5,$,$,$,$,$,$,.ELEMENT.,$,$,0.,$,$); +#24=IFCRELCONTAINEDINSPATIALSTRUCTURE('0mSqFiWyj4IPh$N3UgmOC9',#5,$,$,(#21),#41); +#103= IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Model',*,*,*,*,#11,$,.MODEL_VIEW.,$); ENDSEC; END-ISO-10303-21; diff --git a/e2e/fixtures/allowlisting/not_allowlisted_storeyqto.ifc b/e2e/fixtures/allowlisting/not_allowlisted_storeyqto.ifc new file mode 100644 index 00000000..a65acc2b --- /dev/null +++ b/e2e/fixtures/allowlisting/not_allowlisted_storeyqto.ifc @@ -0,0 +1,49 @@ +ISO-10303-21; +HEADER; +FILE_DESCRIPTION(('ViewDefinition [CoordinationView]'),'2;1'); +FILE_NAME('','2026-02-24T09:56:49',(''),(''),'IfcOpenShell 0.8.0','IfcOpenShell 0.8.0',''); +FILE_SCHEMA(('IFC4')); +ENDSEC; +DATA; +#1=IFCPERSON($,'User','Generated',$,$,$,$,$); +#2=IFCORGANIZATION($,'Example Org',$,$,$); +#3=IFCPERSONANDORGANIZATION(#1,#2,$); +#4=IFCAPPLICATION(#2,'1.0','IfcOpenShell','ifcopenshell'); +#5=IFCOWNERHISTORY(#3,#4,$,.NOTDEFINED.,$,$,$,1771923409); +#6=IFCSIUNIT(*,.LENGTHUNIT.,$,.METRE.); +#7=IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.); +#8=IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.); +#9=IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.); +#10=IFCUNITASSIGNMENT((#6,#7,#8,#9)); +#11=IFCCARTESIANPOINT((0.,0.,0.)); +#12=IFCDIRECTION((0.,0.,1.)); +#13=IFCDIRECTION((1.,0.,0.)); +#14=IFCAXIS2PLACEMENT3D(#11,#12,#13); +#15=IFCGEOMETRICREPRESENTATIONCONTEXT('Model','Model',3,1.E-05,#14,$); +#16=IFCPROJECT('1iyfGnLr1A5QObe35JynRA',#5,'Example Project',$,$,$,$,(#15),#10); +#17=IFCCARTESIANPOINT((0.,0.,0.)); +#18=IFCDIRECTION((0.,0.,1.)); +#19=IFCDIRECTION((1.,0.,0.)); +#20=IFCAXIS2PLACEMENT3D(#17,#18,#19); +#21=IFCLOCALPLACEMENT($,#20); +#22=IFCSITE('0xq5s0Sef4eBW2L6xclQoY',#5,'Site',$,$,#21,$,$,.ELEMENT.,$,$,$,$,$); +#23=IFCCARTESIANPOINT((0.,0.,0.)); +#24=IFCDIRECTION((0.,0.,1.)); +#25=IFCDIRECTION((1.,0.,0.)); +#26=IFCAXIS2PLACEMENT3D(#23,#24,#25); +#27=IFCLOCALPLACEMENT(#21,#26); +#28=IFCBUILDING('19vP5ojgzA4Ak8tpTDmsFk',#5,'Building',$,$,#27,$,$,.ELEMENT.,$,$,$); +#29=IFCCARTESIANPOINT((0.,0.,0.)); +#30=IFCDIRECTION((0.,0.,1.)); +#31=IFCDIRECTION((1.,0.,0.)); +#32=IFCAXIS2PLACEMENT3D(#29,#30,#31); +#33=IFCLOCALPLACEMENT(#27,#32); +#34=IFCBUILDINGSTOREY('0udBjQTrz6qesrXheLTe9j',#5,'Level 0',$,$,#33,$,$,.ELEMENT.,0.); +#35=IFCRELAGGREGATES('168q3RcXXBpw6F0BU2RPbH',#5,$,$,#16,(#22)); +#36=IFCRELAGGREGATES('0nFUVKgJ11avoRp_pS0krY',#5,$,$,#22,(#28)); +#37=IFCRELAGGREGATES('3XdgFhFOPFaf0jfpU9_z85',#5,$,$,#28,(#34)); +#38=IFCQUANTITYLENGTH('NtHeight',$,$,12.34,$); +#39=IFCELEMENTQUANTITY('3KXvj87n5EZR22_nDKi31k',$,'Qto_BuildingStoreyBaseQuantities',$,'BaseQuantities',(#38)); +#40=IFCRELDEFINESBYPROPERTIES('1ihyW38pfDy8xBvjsE5Dq8',#5,$,$,(#34),#39); +ENDSEC; +END-ISO-10303-21; diff --git a/e2e/fixtures/transformer_type_not_whitelisted.ifc b/e2e/fixtures/allowlisting/not_allowlisted_transformertype.ifc similarity index 82% rename from e2e/fixtures/transformer_type_not_whitelisted.ifc rename to e2e/fixtures/allowlisting/not_allowlisted_transformertype.ifc index ce05f1a4..4ae9cb1d 100644 --- a/e2e/fixtures/transformer_type_not_whitelisted.ifc +++ b/e2e/fixtures/allowlisting/not_allowlisted_transformertype.ifc @@ -28,5 +28,9 @@ DATA; #21=IFCTRANSFORMER('152EkAn$n33hZ6u_M0ABgY',$,$,$,$,$,$,$,$); #23=IFCRELDEFINESBYTYPE('0_sGsYHQLAMgWs1UcXRuBz',$,$,$,(#21),#24); #24=IFCWALLTYPE('2so5A4Lx95i9ZO0y$h3Upk',$,'T',$,$,$,$,$,$,.MOVABLE.); +#25=IFCRELAGGREGATES('1_FiI$EtrAVumQxKPOEZu6',#5,$,$,#20,(#41)); +#41=IFCSITE('060XCG67v9ZO7i9CQZAKvK',#5,$,$,$,$,$,$,.ELEMENT.,$,$,0.,$,$); +#26=IFCRELCONTAINEDINSPATIALSTRUCTURE('0mSqFiWyj4IPh$N3UgmOC9',#5,$,$,(#21),#41); +#103= IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Model',*,*,*,*,#11,$,.MODEL_VIEW.,$); ENDSEC; END-ISO-10303-21; diff --git a/e2e/playwright.config.js b/e2e/playwright.config.js index 4f343da4..530bf69d 100644 --- a/e2e/playwright.config.js +++ b/e2e/playwright.config.js @@ -37,7 +37,12 @@ export default defineConfig({ projects: [ { name: 'e2e-tests', - testMatch: '**/tests/*.test.js', + testMatch: [ + '**/tests/django_admin.test.js', + '**/tests/validate.test.js', + '**/tests/validate_api.test.js', + '**/tests/allowlisting.test.js', + ], use: { ...devices['Desktop Chrome'], // API tests don't need a browser, but we'll use request context @@ -73,7 +78,7 @@ export default defineConfig({ /* Test timeout */ timeout: 60 * 1000, expect: { - timeout: 15 * 1000, + timeout: 30 * 1000, }, /* Output directory for test results */ diff --git a/e2e/tests/allowlisting.test.js b/e2e/tests/allowlisting.test.js new file mode 100644 index 00000000..84b74a9e --- /dev/null +++ b/e2e/tests/allowlisting.test.js @@ -0,0 +1,170 @@ +import { test, expect } from '@playwright/test'; +import { readFileSync } from 'fs'; +import { basename, extname, resolve } from 'path'; + +const BASE_URL = 'http://localhost:3000'; +const BFF_URL = 'http://localhost:8000/bff'; + +const STATUS_COLUMNS = { + schema: 3, + rules: 4, +}; + +const STATUS_ICONS = { + v: 'CheckCircleIcon', + i: 'ErrorIcon', +}; + +const ALLOWLISTING_CASES = [ + { + fixturePath: 'fixtures/allowlisting/allowlisted_proxyqto.ifc', + expectedSchema: 'v', + expectedRules: 'v', + }, + { + fixturePath: 'fixtures/allowlisting/allowlisted_storeyqto.ifc', + expectedSchema: 'v', + expectedRules: 'v', + }, + { + fixturePath: 'fixtures/allowlisting/not_allowlisted_storeyqto.ifc', + expectedSchema: 'v', + expectedRules: 'i', + }, + { + fixturePath: 'fixtures/allowlisting/allowlisted_transformertype.ifc', + expectedSchema: 'v', + expectedRules: 'v', + }, + { + fixturePath: 'fixtures/allowlisting/not_allowlisted_transformertype.ifc', + expectedSchema: 'i', + expectedRules: 'v', + }, +]; + +let createdModelIds = []; + +function createUploadPayload(fixturePath) { + const absolutePath = resolve(process.cwd(), fixturePath); + const originalName = basename(fixturePath); + const extension = extname(originalName); + const stem = originalName.slice(0, -extension.length); + const uniqueSuffix = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`; + + return { + buffer: readFileSync(absolutePath), + uploadName: `${stem}-${uniqueSuffix}${extension}`, + }; +} + +async function gotoDashboard(page) { + await page.goto(`${BASE_URL}/dashboard`, { waitUntil: 'networkidle' }); + await expect(page.getByRole('columnheader', { name: 'File Name' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'Upload & Validate' })).toBeVisible(); + await expect(page.locator('input[type="file"]').first()).toBeAttached(); +} + +async function uploadFixture(page, fixturePath) { + const { buffer, uploadName } = createUploadPayload(fixturePath); + + await gotoDashboard(page); + await page.locator('input[type="file"]').first().setInputFiles({ + name: uploadName, + mimeType: 'application/octet-stream', + buffer, + }); + await page.getByRole('button', { name: 'Upload & Validate' }).click(); + + const row = page.locator('tbody tr').filter({ hasText: uploadName }).first(); + await expect(row).toBeVisible({ timeout: 60_000 }); + + return uploadName; +} + +async function getModelByFilename(page, fileName) { + const response = await page.request.get(`${BFF_URL}/api/models_paginated/0/25`); + expect(response.ok()).toBeTruthy(); + + const data = await response.json(); + return data.models.find((model) => model.filename === fileName) ?? null; +} + +async function waitForCompletedModel(page, fileName) { + let latestModel = null; + + await expect + .poll( + async () => { + latestModel = await getModelByFilename(page, fileName); + return latestModel?.progress ?? 'missing'; + }, + { + timeout: 180_000, + intervals: [1_000, 2_000, 5_000], + } + ) + .toBe(100); + + return latestModel; +} + +async function expectStatusIcon(row, columnKey, status) { + const iconTestId = STATUS_ICONS[status]; + const cell = row.locator('td').nth(STATUS_COLUMNS[columnKey]); + + await expect(cell.locator(`[data-testid="${iconTestId}"]`)).toBeVisible(); +} + +async function deleteCreatedModels(page) { + if (createdModelIds.length === 0) { + return; + } + + const cookies = await page.context().cookies(); + const csrfToken = cookies.find((cookie) => cookie.name === 'csrftoken')?.value; + + if (!csrfToken) { + createdModelIds = []; + return; + } + + const response = await page.request.delete(`${BFF_URL}/api/delete/${createdModelIds.join(',')}`, { + headers: { + 'x-csrf-token': csrfToken, + }, + }); + + expect(response.ok()).toBeTruthy(); + createdModelIds = []; +} + +test.describe('UI - Allowlisting dashboard statuses', () => { + test.describe.configure({ mode: 'serial' }); + + test.afterEach(async ({ page }) => { + await deleteCreatedModels(page); + }); + + for (const allowlistingCase of ALLOWLISTING_CASES) { + test(`shows the expected schema and rules status for ${basename(allowlistingCase.fixturePath)}`, async ({ page }) => { + test.slow(); + + const uploadedFileName = await uploadFixture(page, allowlistingCase.fixturePath); + const model = await waitForCompletedModel(page, uploadedFileName); + + expect(model).not.toBeNull(); + createdModelIds.push(model.id); + + expect(model.status_schema).toBe(allowlistingCase.expectedSchema); + expect(model.status_rules).toBe(allowlistingCase.expectedRules); + + await gotoDashboard(page); + + const row = page.locator('tbody tr').filter({ hasText: uploadedFileName }).first(); + await expect(row).toBeVisible(); + await expectStatusIcon(row, 'schema', allowlistingCase.expectedSchema); + await expectStatusIcon(row, 'rules', allowlistingCase.expectedRules); + }); + } +}); diff --git a/e2e/tests/global-setup.js b/e2e/tests/global-setup.js index 07dc28b6..cd515014 100644 --- a/e2e/tests/global-setup.js +++ b/e2e/tests/global-setup.js @@ -1,11 +1,20 @@ +import { chromium } from '@playwright/test'; -// tests/global-setup.js async function globalSetup(config) { - console.log('🚀 Starting global setup for Playwright tests...'); - - // Any global setup logic can go here - // For example, seeding test data, setting up test databases, etc. - + console.log('🚀 Starting global setup...'); + + // Warm up the frontend by loading it in a real browser + // This triggers JS bundle compilation on the dev server + const browser = await chromium.launch(); + const page = await browser.newPage(); + try { + await page.goto('http://localhost:3000', { waitUntil: 'networkidle', timeout: 60_000 }); + console.log('✅ Frontend warmed up'); + } catch (e) { + console.warn('⚠️ Frontend warmup timed out:', e.message); + } + await browser.close(); + console.log('✅ Global setup completed'); } diff --git a/e2e/tests/validate.test.js b/e2e/tests/validate.test.js index 1ab272dd..a2f4c568 100644 --- a/e2e/tests/validate.test.js +++ b/e2e/tests/validate.test.js @@ -8,7 +8,7 @@ test.describe('Validate WebUI Tests', () => { test('should be able to see the homepage', async ({ page }) => { // navigate to the Validate Web UI - await page.goto(BASE_URL); + await page.goto(BASE_URL, { waitUntil: 'networkidle' }); // check if certain elements are visible // a left-hand side menu, a text element and an upload button @@ -20,7 +20,7 @@ test.describe('Validate WebUI Tests', () => { test('should be able to see the dashboard', async ({ page }) => { // navigate to the Validate Web UI - await page.goto(BASE_URL); + await page.goto(BASE_URL, { waitUntil: 'networkidle' }); // click the dashboard link await page.getByRole('link', { name: 'Validation', exact: true }).click(); @@ -38,7 +38,7 @@ test.describe('Validate WebUI Tests', () => { await page.context().clearCookies(); // navigate to the Validate Web UI - await page.goto(BASE_URL); + await page.goto(BASE_URL, { waitUntil: 'networkidle' }); // check for a specific cookie by name; retry with delay if not found let retries = 5; @@ -61,7 +61,7 @@ test.describe('Validate WebUI Tests', () => { await page.context().clearCookies(); // navigate to the Validate Web UI - await page.goto(BASE_URL); + await page.goto(BASE_URL, { waitUntil: 'networkidle' }); // wait for a specific cookie by name; retry with delay if not found let retries = 5; diff --git a/frontend/src/SchemaResult.js b/frontend/src/SchemaResult.js index 9df81dca..0f30d534 100644 --- a/frontend/src/SchemaResult.js +++ b/frontend/src/SchemaResult.js @@ -12,6 +12,7 @@ import TableContainer from '@mui/material/TableContainer'; import TableHead from '@mui/material/TableHead'; import TablePagination from '@mui/material/TablePagination'; import { statusToColor, severityToColor, severityToLabel, statusToLabel } from './mappings'; +import { Link } from "react-router-dom"; function coerceToStr(v) { if (!v) { @@ -158,7 +159,10 @@ export default function SchemaResult({ summary, count, content, status, instance {instances[row.instance_id] ? instances[row.instance_id].guid : '-'} {instances[row.instance_id] ? instances[row.instance_id].type : '-'} - {severityToLabel[row.severity]} + {row.allowlisted + ? ⓘ Allowlisted + : {severityToLabel[row.severity]} + } {featureDescription && row.expected && row.observed