294 lines
20 KiB
JavaScript
294 lines
20 KiB
JavaScript
// Run with an installed Playwright package. PLAYWRIGHT_MODULE can point to an external installation.
|
|
// SCREENSHOT_DIR optionally selects where the browser captures are written.
|
|
import fs from "node:fs";
|
|
import assert from "node:assert/strict";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import {createRequire} from "node:module";
|
|
import {fileURLToPath} from "node:url";
|
|
const require = createRequire(import.meta.url);
|
|
const {chromium} = require(process.env.PLAYWRIGHT_MODULE || "playwright");
|
|
const root = fileURLToPath(new URL("../app/web/", import.meta.url));
|
|
const out = process.env.SCREENSHOT_DIR || fs.mkdtempSync(path.join(os.tmpdir(), "adfs-ui-review-"));
|
|
fs.mkdirSync(out, {recursive: true});
|
|
const data={initialized:true,fetchedAt:new Date().toISOString(),users:[
|
|
{sid:'alice',sam:'alice',name:'Alice Schneider',available:true},
|
|
{sid:'bob',sam:'bob',name:'Robert Müller',available:true},
|
|
{sid:'carol',sam:'carol',name:'Carol Fischer',available:true},
|
|
...Array.from({length:77},(_,i)=>({sid:'user-'+i,sam:'mitarbeiter.'+String(i+1).padStart(2,'0'),name:'Mitarbeiter '+(i+1),available:true}))
|
|
],folders:[
|
|
{id:'invoices',name:'Rechnungen',active:true,permissions:[{kind:'user',principalId:'alice',level:2},{kind:'user',principalId:'bob',level:1},{kind:'user',principalId:'carol',level:0}]},
|
|
{id:'projects',name:'Projektunterlagen und technische Dokumentation internationaler Niederlassungen für das laufende Geschäftsjahr',active:true,permissions:[{kind:'user',principalId:'carol',level:3}]},
|
|
{id:'archive',name:'Abschluss 2024',active:false,permissions:[]}
|
|
]};
|
|
const trashItems=[
|
|
{id:'data/report',share:'Data',path:'Finance/Budget.xlsx',size:14336,deletedAt:'2026-10-03T09:20:00Z',deletedBy:'EXAMPLE\\alice',expiresAt:'2026-10-10T09:20:00Z'},
|
|
{id:'private/note',share:'Private',path:'Notizen.txt',size:1536,deletedAt:'2026-10-03T08:45:00Z',deletedBy:'EXAMPLE\\bob',expiresAt:'2026-10-10T08:45:00Z'}
|
|
];
|
|
let nextId=0, failSave=false; const requests=[],errors=[],restores=[];
|
|
(async()=>{
|
|
const browser=await chromium.launch({headless:true});
|
|
const page=await browser.newPage({viewport:{width:1500,height:1050}});
|
|
page.on('pageerror',e=>errors.push(e.message));
|
|
await page.route('http://adfs.test/**',async route=>{
|
|
const url=new URL(route.request().url());let body,type='application/json',status=200;
|
|
if(url.pathname==='/api/session')body={user:'EXAMPLE\\admin',expiresAt:9999999999};
|
|
else if(url.pathname==='/api/access'){
|
|
if(route.request().method()==='POST'){
|
|
const change=route.request().postDataJSON();requests.push(change);
|
|
if(failSave){failSave=false;return route.fulfill({status:409,contentType:type,body:JSON.stringify({error:'Freigabenabgleich läuft; bitte erneut versuchen'})});}
|
|
if(change.action==='set-permissions')data.folders.find(f=>f.id===change.id).permissions=change.permissions;
|
|
if(change.action==='create-folder')data.folders.push({id:'folder-'+(++nextId),name:change.name,active:true,permissions:[]});
|
|
if(change.action==='archive-folder'||change.action==='restore-folder')data.folders.find(f=>f.id===change.id).active=change.action==='restore-folder';
|
|
}body=data;
|
|
} else if(url.pathname==='/api/trash')body={items:trashItems,matched:trashItems.length,retentionDays:7,truncated:false};
|
|
else if(url.pathname==='/api/trash/restore'){
|
|
const change=route.request().postDataJSON();restores.push(change);
|
|
const item=trashItems.find(item=>item.id===change.id);
|
|
trashItems.splice(trashItems.indexOf(item),1);body={share:item.share,path:item.path};
|
|
} else if(url.pathname==='/assets/app.js'){body=fs.readFileSync(root+'/app.js');type='text/javascript';}
|
|
else if(url.pathname==='/assets/styles.css'){body=fs.readFileSync(root+'/styles.css');type='text/css';}
|
|
else {body=fs.readFileSync(root+'/index.html');type='text/html';}
|
|
await route.fulfill({status,contentType:type,body:Buffer.isBuffer(body)?body:JSON.stringify(body)});
|
|
});
|
|
const settled=()=>page.waitForFunction(()=>document.querySelector('#access-message')?.textContent==='Gespeichert. Die Änderungen sind wirksam.'&&!document.querySelector('#access-create').disabled);
|
|
const shot=async name=>page.screenshot({path:out+'/'+name+'.png',fullPage:true});
|
|
const checkShareNameEllipsis=async()=>{
|
|
const longName=page.locator('[data-select=projects] strong');
|
|
assert.equal(await longName.getAttribute('title'),data.folders.find(f=>f.id==='projects').name);
|
|
assert.equal(await longName.evaluate(element=>element.scrollWidth>element.clientWidth),true,'Long share name is not clipped');
|
|
const heights=await page.evaluate(()=>({
|
|
long:document.querySelector('[data-select=projects]').getBoundingClientRect().height,
|
|
short:document.querySelector('[data-select=invoices]').getBoundingClientRect().height
|
|
}));
|
|
assert.ok(Math.abs(heights.long-heights.short)<=1,'Long share name wraps and increases row height');
|
|
};
|
|
const checkSharedStyle=async()=>{
|
|
const differences=await page.evaluate(()=>{
|
|
const mismatches=[];
|
|
const compare=(actual,reference,properties)=>{
|
|
const a=getComputedStyle(document.querySelector(actual)),b=getComputedStyle(document.querySelector(reference));
|
|
for(const property of properties)if(a[property]!==b[property])mismatches.push(`${actual}: ${property} ${a[property]} != ${b[property]}`);
|
|
};
|
|
const controls=['fontFamily','fontSize','color','backgroundColor','borderTopColor','borderTopWidth','borderTopStyle','borderRadius','boxShadow','padding'];
|
|
compare('#access-create','#login-form button',controls);
|
|
compare('#access-search','#login-form input[name=username]',controls);
|
|
const listReference=document.createElement('button');
|
|
listReference.id='shared-list-reference';listReference.className='select-row active';
|
|
document.body.append(listReference);
|
|
compare('.access-list-item.active','#shared-list-reference',['backgroundColor','fontWeight']);
|
|
listReference.remove();
|
|
if(document.querySelector('.access-level-options'))compare('.access-level-options button[aria-pressed="true"]','nav a.active',['backgroundColor','fontWeight']);
|
|
if(document.querySelector('.access-dialog')){
|
|
compare('.access-dialog input','#login-form input[name=username]',controls);
|
|
compare('.access-dialog button[type=submit]','#login-form button',controls);
|
|
}
|
|
return mismatches;
|
|
});
|
|
assert.deepEqual(differences,[],'Access controls differ from the established admin styles');
|
|
};
|
|
await page.goto('http://adfs.test/shares');await page.locator('[data-user-rule]').first().waitFor();
|
|
assert.equal(new URL(page.url()).pathname,'/access');
|
|
assert.equal(await page.locator('nav a[href="/shares"]').count(),0);
|
|
assert.equal(await page.locator('[data-tab=groups], [data-group-rule]').count(),0);
|
|
assert.doesNotMatch(await page.locator('#content').innerText(),/Gruppe/);
|
|
assert.equal(await page.locator('#access-users .access-matrix-row').count(),80);
|
|
await checkSharedStyle();
|
|
await checkShareNameEllipsis();
|
|
await shot('01-desktop-benutzerrechte');
|
|
// Filters remain global to the access page while each folder loads its own rights.
|
|
await page.locator('#access-search').fill('r');
|
|
await page.selectOption('#folder-status','all');
|
|
for(const filter of ['granted','hidden','all']){
|
|
await page.locator('#access-user-search').fill('alice');
|
|
await page.selectOption('#access-user-filter',filter);
|
|
await page.locator('[data-select=projects]').click();
|
|
assert.equal(await page.locator('#access-user-search').inputValue(),'alice');
|
|
assert.equal(await page.locator('#access-user-filter').inputValue(),filter);
|
|
assert.equal(await page.locator('#access-search').inputValue(),'r');
|
|
assert.equal(await page.locator('#folder-status').inputValue(),'all');
|
|
assert.equal(await page.locator('#access-users .access-matrix-row').count(),filter==='granted'?0:1);
|
|
if(filter==='granted'){
|
|
assert.match(await page.locator('#access-users').innerText(),/Keine Benutzer/);
|
|
await shot('08-folder-switch-retains-empty-filter');
|
|
}
|
|
await page.locator('[data-select=invoices]').click();
|
|
assert.equal(await page.locator('#access-user-search').inputValue(),'alice');
|
|
assert.equal(await page.locator('#access-user-filter').inputValue(),filter);
|
|
assert.equal(await page.locator('#access-users .access-matrix-row').count(),filter==='hidden'?0:1);
|
|
}
|
|
await page.locator('#access-search').fill('');
|
|
await page.selectOption('#access-user-filter','granted');
|
|
await page.selectOption('#folder-status','archived');
|
|
assert.equal(await page.locator('#access-user-search').inputValue(),'alice');
|
|
assert.equal(await page.locator('#access-user-filter').inputValue(),'granted');
|
|
assert.equal(await page.locator('#access-users .access-matrix-row').count(),0);
|
|
await page.selectOption('#folder-status','active');
|
|
assert.equal(await page.locator('#access-user-search').inputValue(),'alice');
|
|
assert.equal(await page.locator('#access-user-filter').inputValue(),'granted');
|
|
assert.equal(await page.locator('#access-users .access-matrix-row').count(),1);
|
|
await shot('09-folder-switch-retains-matching-filter');
|
|
await page.locator('#access-user-search').fill('');
|
|
await page.selectOption('#access-user-filter','all');
|
|
assert.equal(await page.locator('#access-save').isDisabled(),true);
|
|
await page.locator('#access-user-search').fill('alice');
|
|
await page.locator('[data-user-rule=alice][data-level="3"]').click();
|
|
assert.equal(await page.locator('#access-save').isEnabled(),true);
|
|
await page.locator('#access-user-search').fill('bob');
|
|
await page.locator('[data-user-rule=bob][data-level="2"]').click();
|
|
await page.locator('#access-user-search').fill('');
|
|
await page.selectOption('#access-user-filter','granted');
|
|
assert.equal(await page.locator('#access-users .access-matrix-row').count(),2);
|
|
assert.equal(await page.locator('[data-user-rule=alice][data-level="3"]').getAttribute('aria-pressed'),'true');
|
|
assert.match(await page.locator('#access-user-count').innerText(),/ungespeicherten/);
|
|
await shot('02-desktop-mit-zugriff');
|
|
let dialogs=0;
|
|
page.on('dialog',async d=>{dialogs++;await d.dismiss();});
|
|
await page.locator('[data-select=projects]').click();
|
|
assert.match(await page.locator('.access-detail-head h2').innerText(),/Rechnungen/);
|
|
assert.equal(dialogs,1);
|
|
await page.selectOption('#folder-status','archived');
|
|
assert.equal(await page.locator('#folder-status').inputValue(),'active');
|
|
assert.equal(dialogs,2);
|
|
await page.locator('a[data-route="storage-data"]').click();
|
|
assert.equal(new URL(page.url()).pathname,'/access');
|
|
assert.equal(dialogs,3);
|
|
failSave=true;await page.locator('#access-save').click();
|
|
await page.locator('.access-message.failure').waitFor();
|
|
assert.equal(await page.locator('[data-user-rule=alice][data-level="3"]').getAttribute('aria-pressed'),'true');
|
|
assert.equal(await page.locator('#access-save').isEnabled(),true);
|
|
await shot('03-desktop-fehler-erhaelt-entwurf');
|
|
await page.locator('#access-save').click();await settled();
|
|
assert.match(await page.locator('.access-detail-head h2').innerText(),/Rechnungen/);
|
|
assert.equal(await page.locator('#access-user-filter').inputValue(),'granted');
|
|
assert.equal(requests.at(-1).permissions.find(r=>r.principalId==='alice').level,3);
|
|
assert.ok(requests.at(-1).permissions.every(r=>r.kind==='user'));
|
|
await page.locator('[data-user-rule=bob][data-level="0"]').click();
|
|
assert.equal(await page.locator('#access-users .access-matrix-row').count(),1);
|
|
await page.selectOption('#access-user-filter','hidden');
|
|
assert.equal(await page.locator('[data-user-rule=bob][data-level="0"]').getAttribute('aria-pressed'),'true');
|
|
await page.locator('#access-discard').click();
|
|
await page.selectOption('#access-user-filter','granted');
|
|
assert.equal(await page.locator('[data-user-rule=bob][data-level="2"]').getAttribute('aria-pressed'),'true');
|
|
assert.equal(await page.locator('#access-save').isDisabled(),true);
|
|
await page.selectOption('#access-user-filter','hidden');
|
|
await page.locator('#access-user-search').fill('alice');
|
|
await page.locator('[data-select=projects]').click();
|
|
assert.equal(await page.locator('#access-user-search').inputValue(),'alice');
|
|
assert.equal(await page.locator('#access-user-filter').inputValue(),'hidden');
|
|
assert.equal(await page.locator('[data-user-rule=alice][data-level="0"]').getAttribute('aria-pressed'),'true');
|
|
await page.locator('[data-user-rule=alice][data-level="1"]').click();
|
|
await page.locator('#access-save').click();await settled();
|
|
await page.locator('[data-select=invoices]').click();
|
|
assert.equal(await page.locator('#access-user-search').inputValue(),'alice');
|
|
assert.equal(await page.locator('#access-user-filter').inputValue(),'hidden');
|
|
assert.equal(await page.locator('#access-users .access-matrix-row').count(),0);
|
|
await page.selectOption('#access-user-filter','granted');
|
|
assert.equal(await page.locator('[data-user-rule=alice][data-level="3"]').getAttribute('aria-pressed'),'true');
|
|
await page.locator('#access-create').click();
|
|
await page.locator('.access-dialog input').fill('Neue Ablage');
|
|
await checkSharedStyle();await shot('04-desktop-ordner-anlegen');
|
|
await page.locator('.access-dialog button[type=submit]').click();await settled();
|
|
assert.match(await page.locator('.access-detail-head h2').innerText(),/Neue Ablage/);
|
|
assert.equal(await page.locator('#access-user-search').inputValue(),'alice');
|
|
assert.equal(await page.locator('#access-user-filter').inputValue(),'granted');
|
|
assert.equal(await page.locator('#access-users .access-matrix-row').count(),0);
|
|
await page.locator('#access-user-search').fill('');
|
|
await page.selectOption('#access-user-filter','all');
|
|
assert.equal(await page.locator('#access-users button[aria-pressed=true][data-level="0"]').count(),80);
|
|
await page.locator('#access-archive').click();
|
|
await page.locator('.access-dialog button[type=submit]').click();await settled();
|
|
assert.equal(await page.locator('#folder-status').inputValue(),'archived');
|
|
assert.equal(await page.locator('[data-user-rule]').first().isDisabled(),true);
|
|
const folderRestore=page.getByRole('button',{name:'Wiederherstellen',exact:true});
|
|
assert.equal(await folderRestore.getAttribute('title'),'Wiederherstellen');
|
|
assert.equal(await folderRestore.innerText(),'');
|
|
await folderRestore.locator('svg').click();
|
|
const confirmRestore=page.locator('.access-dialog').getByRole('button',{name:'Wiederherstellen',exact:true});
|
|
assert.equal(await confirmRestore.innerText(),'');
|
|
await shot('10-folder-restore-icon');
|
|
await confirmRestore.locator('svg').click();await settled();
|
|
assert.equal(await page.locator('#folder-status').inputValue(),'active');
|
|
await page.locator('[data-select=invoices]').click();
|
|
await page.selectOption('#access-user-filter','granted');
|
|
for(const width of [1200,900,390]){
|
|
await page.setViewportSize({width,height:width===390?844:1000});
|
|
assert.equal(await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth),false,'Horizontal overflow at '+width);
|
|
assert.equal(await page.evaluate(()=>{
|
|
const section=document.querySelector('#access-detail .access-section');
|
|
const footer=document.querySelector('.access-savebar');
|
|
return footer.getBoundingClientRect().top >= section.getBoundingClientRect().bottom-1;
|
|
}),true,'Save controls cover form content at '+width);
|
|
await checkSharedStyle();
|
|
await checkShareNameEllipsis();
|
|
await shot('05-benutzerrechte-'+width);
|
|
}
|
|
await page.selectOption('#access-user-filter','all');
|
|
await page.locator('#access-user-search').fill('mitarbeiter.77');
|
|
assert.equal(await page.locator('#access-users .access-matrix-row').count(),1);
|
|
await page.locator('[data-user-rule=user-76][data-level="1"]').click();
|
|
await page.locator('#access-user-search').fill('');
|
|
await page.selectOption('#access-user-filter','granted');
|
|
assert.equal(await page.locator('[data-user-rule=user-76][data-level="1"]').getAttribute('aria-pressed'),'true');
|
|
await shot('06-mobile-benutzersuche');
|
|
await page.locator('#access-discard').click();
|
|
// Cached users who left AD stay visible only while they have an assignment.
|
|
data.users.push({sid:'departed',sam:'ehemalig',name:'Ehemaliger Benutzer',available:false},
|
|
{sid:'unassigned',sam:'unzugeordnet',name:'Nicht zugeordnet',available:false});
|
|
data.folders.find(f=>f.id==='invoices').permissions.push({kind:'user',principalId:'departed',level:1});
|
|
await page.setViewportSize({width:1500,height:1050});
|
|
await page.goto('http://adfs.test/access');
|
|
await page.locator('[data-user-rule=departed]').first().waitFor();
|
|
assert.equal(await page.locator('[data-user-rule=unassigned]').count(),0);
|
|
await page.locator('#access-user-search').fill('ehemalig');
|
|
assert.match(await page.locator('#access-users').innerText(),/Nicht mehr im AD/);
|
|
await page.locator('[data-user-rule=departed][data-level="0"]').click();
|
|
await page.locator('#access-save').click();await settled();
|
|
assert.equal(await page.locator('[data-user-rule=departed]').count(),0);
|
|
// An installation with only archived folders opens the matching list.
|
|
data.folders.forEach(f=>f.active=false);
|
|
await page.goto('http://adfs.test/access');
|
|
await page.locator('#access-archive').waitFor();
|
|
assert.equal(await page.locator('#folder-status').inputValue(),'archived');
|
|
assert.equal(await page.locator('[data-select].active').count(),1);
|
|
assert.equal(await page.locator('[data-user-rule]').first().isDisabled(),true);
|
|
assert.equal(await page.locator('#access-save').isDisabled(),true);
|
|
data.folders.length=0;
|
|
await page.setViewportSize({width:1500,height:1050});
|
|
await page.goto('http://adfs.test/access');
|
|
await page.locator('#access-create').waitFor();
|
|
assert.match(await page.locator('#access-detail').innerText(),/Ersten Ordner anlegen/);
|
|
await shot('07-desktop-leere-installation');
|
|
// Icon controls keep accessible names and activate their original actions.
|
|
await page.goto('http://adfs.test/trash');
|
|
await page.locator('#trash-rows .row-actions').first().waitFor();
|
|
for(const [role,name] of [['link','Herunterladen'],['button','Wiederherstellen']]){
|
|
const control=page.getByRole(role,{name,exact:true}).first();
|
|
assert.equal(await control.getAttribute('title'),name);
|
|
assert.equal(await control.innerText(),'');
|
|
assert.equal(await control.locator('svg').getAttribute('aria-hidden'),'true');
|
|
}
|
|
await shot('11-trash-action-icons-desktop');
|
|
const downloadStarted=page.waitForEvent('download');
|
|
await page.getByRole('link',{name:'Herunterladen',exact:true}).first().locator('svg').click();
|
|
const download=await downloadStarted;
|
|
assert.equal(download.url(),'http://adfs.test/api/trash/download?id=data%2Freport');
|
|
// Downloads leave page interception; verify the destination and cancel the mocked transfer.
|
|
await download.cancel();
|
|
await page.getByRole('button',{name:'Wiederherstellen',exact:true}).first().locator('svg').click();
|
|
assert.equal(restores.length,0,'Cancelling restoration should not submit');
|
|
page.removeAllListeners('dialog');
|
|
page.on('dialog',dialog=>dialog.accept());
|
|
await page.getByRole('button',{name:'Wiederherstellen',exact:true}).first().locator('svg').click();
|
|
await page.waitForFunction(()=>document.querySelectorAll('#trash-rows .row-actions').length===1);
|
|
assert.deepEqual(restores,[{id:'data/report'}]);
|
|
await page.setViewportSize({width:390,height:844});
|
|
assert.equal(await page.evaluate(()=>document.documentElement.scrollWidth>innerWidth),false,'Trash actions cause page overflow');
|
|
await page.locator('.table-wrap').evaluate(table=>{table.scrollLeft=table.scrollWidth;});
|
|
await shot('12-trash-action-icons-mobile');
|
|
assert.deepEqual(errors,[]);
|
|
console.log('PASS: individual permissions, no groups, user search/filter persistence, unsaved-change protection, failure/retry, independent folders, create/archive/restore, desktop/mobile layout, shared admin styling, retired shares route, restore/download icons and actions.');
|
|
console.log('Screenshots: '+out);
|
|
await browser.close();
|
|
})().catch(error=>{console.error(error);process.exit(1)});
|