PlotDirector/PlotLine.Tests/Browser/location-reparent.cjs

107 lines
7.7 KiB
JavaScript

const { chromium } = require('playwright');
const fs = require('node:fs');
const path = require('node:path');
const assert = require('node:assert/strict');
const repo = path.resolve(__dirname, '../..');
(async () => {
const browser = await chromium.launch({ executablePath: process.env.CHROMIUM_PATH });
try {
const page = await browser.newPage();
const errors = [];
page.on('pageerror', error => errors.push(error.message));
await page.route('https://plotdirector.test/**', route => route.fulfill({ contentType: 'text/html', body: '<html><body></body></html>' }));
await page.goto('https://plotdirector.test/Locations');
const layout = fs.readFileSync(path.join(repo, 'PlotLine/Views/Shared/_Layout.cshtml'), 'utf8');
const modalStart = layout.indexOf(' <div class="modal fade" id="plotlineConfirmModal"');
const modal = layout.slice(modalStart, layout.indexOf(' <script', modalStart));
const rows = [ ['1','Room','',''], ['2','Building','',''], ['3','Cupboard','1','Room'], ['4','Annex','2','Building'] ]
.map(([id,name,parent,parentName]) => `<div style="padding:24px;border:1px solid" draggable="true" data-drag-type="location" data-drag-id="${id}" data-drag-label="${name}" data-parent-location-id="${parent}" data-parent-location-name="${parentName}"><a href="/Locations/Details/${id}">${name}</a></div>`).join('');
await page.setContent(`<select data-location-drag-action><option value="parent">Change parent</option><option value="relationship">Create relationship</option></select>
<p data-location-move-error></p><p data-location-move-status></p>
<form action="/Locations/Reparent" method="post" data-location-reparent-form data-confirm-message="Change parent?" data-confirm-button-text="Move location" data-confirm-button-class="btn-primary">
<input type="hidden" name="projectId" value="1"><input type="hidden" name="locationId"><input type="hidden" name="parentLocationId"><input type="hidden" name="expectedParentLocationId">
</form>
<div class="modal fade" tabindex="-1" data-location-relationship-modal><div class="modal-dialog"><section class="modal-content d-none" data-drag-drop-panel><h2 data-drag-panel-title></h2><span data-drag-panel-kicker></span><p data-drag-panel-summary></p>
<p data-location-relationship-error></p><form action="/Locations/DropRelationship" data-drag-form="location-relationship"><input name="FromLocationID"><input name="ToLocationID"><button type="submit">Save relationship</button></form></section></div></div><div style="height:900px"></div><section data-location-tree>${rows}</section><div style="height:2000px"></div>${modal}`);
await page.addStyleTag({ path: path.join(repo,'PlotLine/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css') });
await page.addScriptTag({ path: path.join(repo,'PlotLine/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js') });
// Load the entire production script: an earlier initialization error can disable dragging.
await page.addScriptTag({ path: path.join(repo,'PlotLine/wwwroot/js/site.js') });
await page.evaluate(() => {
window.savedMoves = [];
window.relationshipSaves = 0;
window.failNextSave = false;
window.fetch = async (url, options) => {
if (window.failNextSave) {
window.failNextSave = false;
return new Response(JSON.stringify({error:'The parent has changed. Review the tree.'}), {status:400});
}
if (url.endsWith('/DropRelationship')) {
window.relationshipSaves++;
return new Response(JSON.stringify({message:'Saved'}));
}
const values = Object.fromEntries(options.body);
window.savedMoves.push(values);
const tree = document.querySelector('[data-location-tree]').cloneNode(true);
const source = tree.querySelector(`[data-drag-id="${values.locationId}"]`);
const target = tree.querySelector(`[data-drag-id="${values.parentLocationId}"]`);
source.dataset.parentLocationId = values.parentLocationId;
source.dataset.parentLocationName = target.dataset.dragLabel;
tree.append(source);
return new Response(tree.outerHTML, {headers:{'Content-Type':'text/html'}});
};
});
const drag = async (source, target) => page.locator(`[data-drag-id="${source}"]`).dragTo(page.locator(`[data-drag-id="${target}"]`));
const confirm = page.locator('#plotlineConfirmModal');
await drag(1,2);
await confirm.waitFor({state:'visible'});
assert.match(await confirm.innerText(), /from top level to “Building”/);
assert.equal(await page.evaluate(() => savedMoves.length),0);
await confirm.locator('[data-bs-dismiss="modal"]').last().click();
await confirm.waitFor({state:'hidden'});
await page.locator('.modal-backdrop').waitFor({state:'hidden'});
assert.equal(await page.evaluate(() => savedMoves.length),0);
await drag(1,2);
const scrollBeforeSave = await page.evaluate(() => window.scrollY);
await confirm.locator('[data-confirm-submit]').click();
await confirm.waitFor({state:'hidden'});
await page.locator('.modal-backdrop').waitFor({state:'hidden'});
await page.waitForFunction(() => document.querySelector('[data-location-move-status]').textContent.includes('moved'));
assert.ok(Math.abs(await page.evaluate(() => window.scrollY) - scrollBeforeSave) < 2, 'AJAX update must preserve scroll');
assert.deepEqual(await page.evaluate(() => savedMoves[0]), {projectId:'1',locationId:'1',parentLocationId:'2',expectedParentLocationId:''});
await drag(4,1);
await confirm.waitFor({state:'visible'});
assert.match(await confirm.innerText(), /from Building to “Room”/);
await confirm.locator('[data-confirm-submit]').click();
await confirm.waitFor({state:'hidden'});
await page.locator('.modal-backdrop').waitFor({state:'hidden'});
assert.equal(await page.evaluate(() => savedMoves[1].expectedParentLocationId),'2');
for (const [source,target] of [[1,1],[1,3],[1,2]]) {
await drag(source,target);
assert.equal(await confirm.isVisible(),false);
assert.equal(await page.evaluate(() => savedMoves.length),2);
}
// Server validation failures leave the existing tree intact and explain the error.
await page.evaluate(() => window.failNextSave = true);
await drag(4,2);
await confirm.locator('[data-confirm-submit]').click();
await confirm.waitFor({state:'hidden'});
await page.locator('.modal-backdrop').waitFor({state:'hidden'});
assert.match(await page.locator('[data-location-move-error]').innerText(), /parent has changed/);
assert.equal(await page.locator('[data-drag-id="4"]').getAttribute('data-parent-location-id'),'1');
await page.selectOption('[data-location-drag-action]','relationship');
await drag(1,2);
await page.locator('[data-drag-drop-panel]').waitFor({state:'visible'});
assert.equal(await page.locator('[name="FromLocationID"]').inputValue(),'1');
assert.equal(await page.locator('[name="ToLocationID"]').inputValue(),'2');
const relationshipScroll = await page.evaluate(() => window.scrollY);
await page.locator('[data-drag-form="location-relationship"] button').click();
await page.locator('[data-location-relationship-modal]').waitFor({state:'hidden'});
await page.locator('.modal-backdrop').waitFor({state:'hidden'});
assert.ok(Math.abs(await page.evaluate(() => window.scrollY) - relationshipScroll) < 2);
assert.equal(await page.evaluate(() => relationshipSaves),1);
assert.deepEqual(errors,[]);
console.log('PASS: drag assignment/reassignment, confirmation, cancellation, invalid targets, AJAX refresh, scroll preservation, server errors, relationship modal/AJAX, full site.js initialization');
} finally { await browser.close(); }
})().catch(error => { console.error(error); process.exitCode = 1; });