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: '
' }));
await page.goto('https://plotdirector.test/Locations');
const layout = fs.readFileSync(path.join(repo, 'PlotLine/Views/Shared/_Layout.cshtml'), 'utf8');
const modalStart = layout.indexOf(' `
`).join('');
await page.setContent(`
${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; });