feat(oa): add training-dir workflow and save flow for OnlineAcademy runner
Introduce a simpler training-based workflow for the OnlineAcademy Playwright runner. Changes included: - support --training-dir and automatic loading of content.md and training.json - support --env-file so OA credentials no longer need manual shell sourcing - resolve asset paths relative to the training directory - improve human-readable review and execute output with step-by-step progress - keep the browser open after execute when no save flag is used - add optional --save flow that clicks 'Opslaan als' and then chooses 'Concept' - add a concrete user guide for preparing and running training imports - update handover documentation to reflect the current repo structure and workflow - align the repo around trainings/<name>/content.md, training.json and assets/ - remove reliance on older pilot/test material in the documented main flow
This commit is contained in:
@@ -24,11 +24,181 @@ const MAX_MULTI_RESPONSE_ANSWERS = 10;
|
||||
const MAX_TABLE_COLUMNS = 5;
|
||||
const MAX_TABLE_ROWS = 20;
|
||||
|
||||
function parseEnvText(source) {
|
||||
const values = {};
|
||||
|
||||
for (const rawLine of source.split(/\r?\n/)) {
|
||||
const line = rawLine.trim();
|
||||
if (!line || line.startsWith("#")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*)$/);
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const [, key, rawValue] = match;
|
||||
let value = rawValue.trim();
|
||||
|
||||
if (
|
||||
(value.startsWith('"') && value.endsWith('"')) ||
|
||||
(value.startsWith("'") && value.endsWith("'"))
|
||||
) {
|
||||
value = value.slice(1, -1);
|
||||
}
|
||||
|
||||
values[key] = value;
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
async function loadEnvFile(envFilePath) {
|
||||
const absolutePath = path.resolve(envFilePath);
|
||||
const source = await fs.readFile(absolutePath, "utf8");
|
||||
const values = parseEnvText(source);
|
||||
|
||||
for (const [key, value] of Object.entries(values)) {
|
||||
if (!process.env[key]) {
|
||||
process.env[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
return absolutePath;
|
||||
}
|
||||
|
||||
function logInfo(message) {
|
||||
console.log(`[INFO] ${message}`);
|
||||
}
|
||||
|
||||
function logStep(message) {
|
||||
console.log(`\n[STAP] ${message}`);
|
||||
}
|
||||
|
||||
function logWarn(message) {
|
||||
console.log(`[LET OP] ${message}`);
|
||||
}
|
||||
|
||||
function summarizePlan(plan) {
|
||||
const chapterCount = plan.chapters.length;
|
||||
const pageCount = plan.chapters.reduce((total, chapter) => total + chapter.pages.length, 0);
|
||||
const blockCount = plan.chapters.reduce(
|
||||
(total, chapter) =>
|
||||
total + chapter.pages.reduce((pageTotal, page) => pageTotal + page.blocks.length, 0),
|
||||
0
|
||||
);
|
||||
|
||||
return { chapterCount, pageCount, blockCount };
|
||||
}
|
||||
|
||||
function describeValidationIssues(validationIssues) {
|
||||
if (validationIssues.length === 0) {
|
||||
return "Geen validatieproblemen gevonden.";
|
||||
}
|
||||
return `${validationIssues.length} validatieprobleem/problemen gevonden. Zie validation.json voor details.`;
|
||||
}
|
||||
|
||||
function shortenText(value, maxLength = 60) {
|
||||
const normalized = String(value ?? "").replace(/\s+/g, " ").trim();
|
||||
if (!normalized) {
|
||||
return "";
|
||||
}
|
||||
if (normalized.length <= maxLength) {
|
||||
return normalized;
|
||||
}
|
||||
return `${normalized.slice(0, maxLength - 3)}...`;
|
||||
}
|
||||
|
||||
function summarizeBlock(block) {
|
||||
switch (block.type) {
|
||||
case "text":
|
||||
return shortenText(block.data.title || block.data.content || "tekstblok");
|
||||
case "heading":
|
||||
return shortenText(block.data.title || "kopblok");
|
||||
case "quote":
|
||||
return shortenText(block.data.quote || "quoteblok");
|
||||
case "image":
|
||||
return shortenText(block.data.title || block.data.caption || block.data.sourcePath || "afbeelding");
|
||||
case "table":
|
||||
return shortenText(block.data.title || "tabel");
|
||||
case "multiple-choice":
|
||||
case "multiple-response":
|
||||
case "open-question":
|
||||
return shortenText(block.data.question || block.type);
|
||||
default:
|
||||
return shortenText(block.type);
|
||||
}
|
||||
}
|
||||
|
||||
function resolveTrainingAssetPath(baseDir, assetPath) {
|
||||
if (path.isAbsolute(assetPath)) {
|
||||
return assetPath;
|
||||
}
|
||||
return path.resolve(baseDir, assetPath);
|
||||
}
|
||||
|
||||
function buildFriendlyErrorMessage(error, options) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
const failureArtifact = path.join(options.outputDir, "failure-state.png");
|
||||
|
||||
if (message.includes("OA_EMAIL") || message.includes("OA_PASSWORD")) {
|
||||
return [
|
||||
"De runner kon niet inloggen omdat de credentials ontbreken.",
|
||||
`Controleer ${path.resolve(options.envFile)} en kijk of OA_EMAIL en OA_PASSWORD zijn ingevuld.`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
if (message.includes("Kon env-bestand niet laden")) {
|
||||
return [
|
||||
"Het env-bestand kon niet worden gelezen.",
|
||||
`Controleer of ${path.resolve(options.envFile)} bestaat en leesbaar is.`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
if (message.includes("Geen url gevonden")) {
|
||||
return [
|
||||
"De URL van de training ontbreekt.",
|
||||
`Controleer ${options.trainingConfigPath ?? "training.json"} en vul daar de sleutel "url" in.`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
if (
|
||||
message.includes("locator('[data-testid=\"page-button\"]')") ||
|
||||
message.includes('locator(\'[data-testid="page-button"]\')') ||
|
||||
message.includes('[data-testid="page-button"]')
|
||||
) {
|
||||
return [
|
||||
"De browser heeft de OnlineAcademy-editor niet volledig herkend.",
|
||||
"Verwachting: na het openen en inloggen moet links de hoofdstuk/pagina-navigatie zichtbaar worden.",
|
||||
"Wat waarschijnlijk misging: de pagina is nog niet volledig geladen, de URL opent niet de editor, of er staat nog een tussenstap open na het inloggen.",
|
||||
`Controleer de browser en kijk ook naar ${failureArtifact}.`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
if (message.includes('[data-testid="editor-main"]')) {
|
||||
return [
|
||||
"De editor is niet zichtbaar geworden.",
|
||||
"Verwachting: het hoofdcanvas van de editor moet in beeld komen.",
|
||||
`Controleer de browser en kijk ook naar ${failureArtifact}.`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
return [
|
||||
"De run is gestopt door een fout.",
|
||||
`Technische melding: ${message}`,
|
||||
`Controleer de browser en kijk naar ${failureArtifact}.`,
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const options = {
|
||||
trainingDir: null,
|
||||
markdownPath: null,
|
||||
targetUrl: null,
|
||||
envFile: ".env",
|
||||
execute: false,
|
||||
save: false,
|
||||
pauseAtEnd: false,
|
||||
headed: true,
|
||||
slowMo: 250,
|
||||
@@ -37,12 +207,18 @@ function parseArgs(argv) {
|
||||
|
||||
for (let i = 2; i < argv.length; i += 1) {
|
||||
const arg = argv[i];
|
||||
if (arg === "--markdown") {
|
||||
if (arg === "--training-dir") {
|
||||
options.trainingDir = argv[++i];
|
||||
} else if (arg === "--markdown") {
|
||||
options.markdownPath = argv[++i];
|
||||
} else if (arg === "--url") {
|
||||
options.targetUrl = argv[++i];
|
||||
} else if (arg === "--env-file") {
|
||||
options.envFile = argv[++i];
|
||||
} else if (arg === "--execute") {
|
||||
options.execute = true;
|
||||
} else if (arg === "--save") {
|
||||
options.save = true;
|
||||
} else if (arg === "--pause-at-end") {
|
||||
options.pauseAtEnd = true;
|
||||
} else if (arg === "--headless") {
|
||||
@@ -56,16 +232,53 @@ function parseArgs(argv) {
|
||||
}
|
||||
}
|
||||
|
||||
if (!options.markdownPath) {
|
||||
throw new Error("Gebruik --markdown <bestand.md>.");
|
||||
if (options.trainingDir) {
|
||||
const resolvedTrainingDir = path.resolve(options.trainingDir);
|
||||
options.trainingDir = resolvedTrainingDir;
|
||||
options.markdownPath = path.join(resolvedTrainingDir, "content.md");
|
||||
options.trainingConfigPath = path.join(resolvedTrainingDir, "training.json");
|
||||
|
||||
if (options.outputDir === path.resolve("artifacts/oa-runner")) {
|
||||
options.outputDir = path.resolve("artifacts", path.basename(resolvedTrainingDir));
|
||||
}
|
||||
}
|
||||
if (!options.targetUrl) {
|
||||
throw new Error("Gebruik --url <onlineacademy-edit-url>.");
|
||||
|
||||
if (!options.markdownPath) {
|
||||
throw new Error(
|
||||
"Gebruik --training-dir <map> of --markdown <bestand.md>."
|
||||
);
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
async function applyTrainingConfig(options) {
|
||||
if (!options.trainingConfigPath) {
|
||||
if (!options.targetUrl) {
|
||||
throw new Error(
|
||||
"Gebruik --url <onlineacademy-edit-url> of werk met --training-dir <map> met training.json."
|
||||
);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
const rawConfig = await fs.readFile(options.trainingConfigPath, "utf8");
|
||||
const config = JSON.parse(rawConfig);
|
||||
const targetUrl = options.targetUrl ?? config.url;
|
||||
|
||||
if (!targetUrl) {
|
||||
throw new Error(
|
||||
`Geen url gevonden. Vul "url" in in ${options.trainingConfigPath} of geef --url mee.`
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...options,
|
||||
targetUrl,
|
||||
trainingConfig: config,
|
||||
};
|
||||
}
|
||||
|
||||
function buildPlan(courseModel) {
|
||||
return {
|
||||
training: courseModel.title,
|
||||
@@ -120,10 +333,63 @@ async function waitForUserReview() {
|
||||
rl.close();
|
||||
}
|
||||
|
||||
async function clickSaveButton(page) {
|
||||
const candidates = [
|
||||
page.locator(".save-button button").first(),
|
||||
page.locator("button:has(.button__text:text-is('Opslaan als'))").first(),
|
||||
page.locator("button").filter({ hasText: "Opslaan als" }).first(),
|
||||
];
|
||||
|
||||
let clicked = false;
|
||||
for (const candidate of candidates) {
|
||||
const isVisible = await candidate.isVisible().catch(() => false);
|
||||
if (!isVisible) {
|
||||
continue;
|
||||
}
|
||||
await candidate.click();
|
||||
clicked = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!clicked) {
|
||||
throw new Error("Knop 'Opslaan als' niet gevonden of niet zichtbaar.");
|
||||
}
|
||||
|
||||
await page.waitForLoadState("networkidle").catch(() => {});
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
const conceptCandidates = [
|
||||
page.getByRole("button", { name: /^Concept$/i }).first(),
|
||||
page.getByRole("menuitem", { name: /^Concept$/i }).first(),
|
||||
page.locator("button").filter({ hasText: /^Concept$/ }).first(),
|
||||
page.locator("[role='menuitem']").filter({ hasText: /^Concept$/ }).first(),
|
||||
page.locator("text=Concept").first(),
|
||||
];
|
||||
|
||||
let conceptClicked = false;
|
||||
for (const candidate of conceptCandidates) {
|
||||
const isVisible = await candidate.isVisible().catch(() => false);
|
||||
if (!isVisible) {
|
||||
continue;
|
||||
}
|
||||
await candidate.click();
|
||||
conceptClicked = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if (!conceptClicked) {
|
||||
throw new Error("Keuze 'Concept' niet gevonden of niet zichtbaar na 'Opslaan als'.");
|
||||
}
|
||||
|
||||
await page.waitForLoadState("networkidle").catch(() => {});
|
||||
await page.waitForTimeout(2000);
|
||||
}
|
||||
|
||||
async function loginIfNeeded(page, email, password) {
|
||||
await page.waitForLoadState("networkidle").catch(() => {});
|
||||
const passwordField = page.locator('input[type="password"]');
|
||||
if ((await passwordField.count()) === 0) {
|
||||
logInfo("Geen loginformulier gevonden. Waarschijnlijk bestaat er al een geldige sessie.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -151,10 +417,31 @@ async function loginIfNeeded(page, email, password) {
|
||||
|
||||
async function verifyEditor(page) {
|
||||
await page.waitForLoadState("networkidle").catch(() => {});
|
||||
const editorMain = page.locator('[data-testid="editor-main"]');
|
||||
const editorMain = page.locator('[data-testid="editor-main"], .page__content, .page-blocks').first();
|
||||
await editorMain.waitFor({ state: "visible", timeout: 15000 });
|
||||
const chapterButton = page.locator('[data-testid="page-button"]');
|
||||
await chapterButton.waitFor({ state: "visible", timeout: 15000 });
|
||||
|
||||
const leftSidebar = page.locator(".sidebar-left, [class*='sidebar-left']").first();
|
||||
await leftSidebar.waitFor({ state: "visible", timeout: 15000 });
|
||||
|
||||
const blockPanel = page
|
||||
.locator('[data-testid="drag-block-text"], .content-block, .dragblock-group__title, .sidebar-right')
|
||||
.first();
|
||||
await blockPanel.waitFor({ state: "visible", timeout: 15000 });
|
||||
|
||||
const pageCreateButton = page
|
||||
.locator(".sidebar-left-actions button")
|
||||
.filter({ hasText: "Pagina" })
|
||||
.first();
|
||||
const hasPageCreateButton = await pageCreateButton.isVisible().catch(() => false);
|
||||
|
||||
const pageListItem = page.locator(".sidebar-left .editor-chapter-wrapper, .sidebar-left [data-testid='page-button']").first();
|
||||
const hasPageListItem = await pageListItem.isVisible().catch(() => false);
|
||||
|
||||
if (!hasPageCreateButton && !hasPageListItem) {
|
||||
throw new Error(
|
||||
"Editor gevonden, maar geen paginanavigatie of paginaknop zichtbaar."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function getCurrentTrainingTitle(page) {
|
||||
@@ -430,13 +717,17 @@ async function fillQuoteBlock(insertedBlock, block) {
|
||||
await fields.nth(1).fill(block.data.author ?? "");
|
||||
}
|
||||
|
||||
async function fillImageBlock(page, insertedBlock, block) {
|
||||
async function fillImageBlock(page, insertedBlock, block, contentBaseDir) {
|
||||
if (!block.data.sourcePath) {
|
||||
throw new Error("Afbeeldingsblok mist bronbestand.");
|
||||
}
|
||||
|
||||
const absolutePath = path.resolve(block.data.sourcePath);
|
||||
await fs.access(absolutePath);
|
||||
const absolutePath = resolveTrainingAssetPath(contentBaseDir, block.data.sourcePath);
|
||||
await fs.access(absolutePath).catch(() => {
|
||||
throw new Error(
|
||||
`Afbeelding niet gevonden: ${block.data.sourcePath}. Verwacht pad: ${absolutePath}`
|
||||
);
|
||||
});
|
||||
|
||||
const fileInput = insertedBlock.locator("input[type='file']").first();
|
||||
if ((await fileInput.count()) === 0) {
|
||||
@@ -861,7 +1152,7 @@ async function fillQuestionBlock(insertedBlock, block) {
|
||||
await textareas.nth(0).fill(block.data.question);
|
||||
}
|
||||
|
||||
async function fillBlock(page, insertedBlock, block) {
|
||||
async function fillBlock(page, insertedBlock, block, contentBaseDir) {
|
||||
switch (block.type) {
|
||||
case "text":
|
||||
await fillTextBlock(insertedBlock, block);
|
||||
@@ -873,7 +1164,7 @@ async function fillBlock(page, insertedBlock, block) {
|
||||
await fillQuoteBlock(insertedBlock, block);
|
||||
return;
|
||||
case "image":
|
||||
await fillImageBlock(page, insertedBlock, block);
|
||||
await fillImageBlock(page, insertedBlock, block, contentBaseDir);
|
||||
return;
|
||||
case "table":
|
||||
await fillTableBlock(page, insertedBlock, block);
|
||||
@@ -910,25 +1201,36 @@ function blockTypeToTestId(type) {
|
||||
return testId;
|
||||
}
|
||||
|
||||
async function executePlan(page, courseModel, outputDir) {
|
||||
async function executePlan(page, courseModel, outputDir, contentBaseDir) {
|
||||
if (courseModel.chapters.length === 0) {
|
||||
throw new Error("Geen hoofdstuk/pagina in markdownmodel.");
|
||||
}
|
||||
|
||||
const executableChapters = courseModel.chapters.filter((chapter) => chapter.pages.length > 0);
|
||||
const totalChapters = executableChapters.length;
|
||||
const totalPages = executableChapters.reduce((total, chapter) => total + chapter.pages.length, 0);
|
||||
let completedPages = 0;
|
||||
let pageSequence = 0;
|
||||
|
||||
for (let chapterIndex = 0; chapterIndex < courseModel.chapters.length; chapterIndex += 1) {
|
||||
const chapter = courseModel.chapters[chapterIndex];
|
||||
for (let chapterIndex = 0; chapterIndex < executableChapters.length; chapterIndex += 1) {
|
||||
const chapter = executableChapters[chapterIndex];
|
||||
if (chapter.pages.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
logInfo(
|
||||
`Hoofdstuk ${chapterIndex + 1} van ${totalChapters}: ${chapter.title} (${chapter.pages.length} pagina's)`
|
||||
);
|
||||
await createChapter(page, chapter.title);
|
||||
await capture(page, outputDir, `chapter-${chapterIndex + 1}-structure`);
|
||||
|
||||
for (let i = 0; i < chapter.pages.length; i += 1) {
|
||||
const modelPage = chapter.pages[i];
|
||||
pageSequence += 1;
|
||||
completedPages += 1;
|
||||
logInfo(
|
||||
`Pagina ${completedPages} van ${totalPages}: ${modelPage.title} (${modelPage.blocks.length} blokken)`
|
||||
);
|
||||
if (i === 0) {
|
||||
await renameCurrentPage(page, modelPage.title);
|
||||
} else {
|
||||
@@ -942,12 +1244,15 @@ async function executePlan(page, courseModel, outputDir) {
|
||||
throw new Error(`Bloktype ${block.type} valt buiten MVP UI-scope.`);
|
||||
}
|
||||
|
||||
logInfo(
|
||||
`Blok ${j + 1} van ${modelPage.blocks.length}: ${block.type} - ${summarizeBlock(block)}`
|
||||
);
|
||||
const beforeCount = await blockCards(page).count();
|
||||
const beforeSnapshot = await snapshotBlocks(page);
|
||||
await dragBlockIntoCanvas(page, blockTypeToTestId(block.type));
|
||||
await waitForNewBlockCount(page, beforeCount);
|
||||
const insertedBlock = await findInsertedBlock(page, beforeSnapshot);
|
||||
await fillBlock(page, insertedBlock, block);
|
||||
await fillBlock(page, insertedBlock, block, contentBaseDir);
|
||||
await capture(page, outputDir, `page-${pageSequence}-block-${j + 1}-${block.type}`);
|
||||
}
|
||||
}
|
||||
@@ -955,29 +1260,48 @@ async function executePlan(page, courseModel, outputDir) {
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const options = parseArgs(process.argv);
|
||||
let options = parseArgs(process.argv);
|
||||
logStep("Voorbereiden");
|
||||
logInfo(`Trainingsmap: ${options.trainingDir ?? "niet opgegeven"}`);
|
||||
logInfo(`Env-bestand: ${path.resolve(options.envFile)}`);
|
||||
await loadEnvFile(options.envFile).catch((error) => {
|
||||
if (options.envFile === ".env" && error?.code === "ENOENT") {
|
||||
return null;
|
||||
}
|
||||
throw new Error(`Kon env-bestand niet laden: ${path.resolve(options.envFile)}`);
|
||||
});
|
||||
options = await applyTrainingConfig(options);
|
||||
const email = process.env.OA_EMAIL;
|
||||
const password = process.env.OA_PASSWORD;
|
||||
if (!email || !password) {
|
||||
throw new Error("Zet OA_EMAIL en OA_PASSWORD in de omgeving.");
|
||||
throw new Error(
|
||||
`Zet OA_EMAIL en OA_PASSWORD in ${path.resolve(options.envFile)} of in de omgeving.`
|
||||
);
|
||||
}
|
||||
|
||||
const markdown = await fs.readFile(options.markdownPath, "utf8");
|
||||
const courseModel = parseMarkdownToCourseModel(markdown);
|
||||
const validationIssues = validateCourseModel(courseModel, SUPPORTED_UI_BLOCKS);
|
||||
const plan = buildPlan(courseModel);
|
||||
const planSummary = summarizePlan(plan);
|
||||
|
||||
await ensureDir(options.outputDir);
|
||||
await writeJson(path.join(options.outputDir, "plan.json"), plan);
|
||||
await writeJson(path.join(options.outputDir, "validation.json"), validationIssues);
|
||||
|
||||
console.log("Plan:");
|
||||
console.log(JSON.stringify(plan, null, 2));
|
||||
|
||||
if (validationIssues.length > 0) {
|
||||
console.log("Validatieproblemen:");
|
||||
console.log(JSON.stringify(validationIssues, null, 2));
|
||||
}
|
||||
logInfo(`Doel-URL: ${options.targetUrl}`);
|
||||
logInfo(
|
||||
`Inhoud gelezen: ${planSummary.chapterCount} hoofdstuk(ken), ${planSummary.pageCount} pagina('s), ${planSummary.blockCount} blok(ken).`
|
||||
);
|
||||
logInfo(describeValidationIssues(validationIssues));
|
||||
logInfo(`Artifacts worden opgeslagen in: ${options.outputDir}`);
|
||||
logInfo(
|
||||
options.execute
|
||||
? options.save
|
||||
? "Modus: execute + save. De runner bouwt de inhoud op, klikt op 'Opslaan als' en kiest daarna 'Concept'."
|
||||
: "Modus: execute zonder save. De runner bouwt de inhoud op en laat het scherm daarna open voor controle."
|
||||
: "Modus: review. De runner controleert login en editor, maar voert geen invoeracties uit."
|
||||
);
|
||||
|
||||
const browser = await chromium.launch({
|
||||
headless: !options.headed,
|
||||
@@ -990,16 +1314,26 @@ async function main() {
|
||||
});
|
||||
const page = await context.newPage();
|
||||
try {
|
||||
logStep("Browser openen");
|
||||
await page.goto(options.targetUrl, { waitUntil: "domcontentloaded" });
|
||||
logStep("Inloggen");
|
||||
await loginIfNeeded(page, email, password);
|
||||
logStep("Editor controleren");
|
||||
await verifyEditor(page);
|
||||
await capture(page, options.outputDir, "editor-ready");
|
||||
|
||||
const currentTitle = await getCurrentTrainingTitle(page);
|
||||
console.log(`Huidige geopende paginatitel: ${currentTitle}`);
|
||||
logInfo(
|
||||
currentTitle
|
||||
? `Editor gevonden. Huidige geopende paginatitel: ${currentTitle}`
|
||||
: "Editor gevonden."
|
||||
);
|
||||
|
||||
if (!options.execute) {
|
||||
console.log("Reviewmodus klaar. Geen muterende acties uitgevoerd.");
|
||||
logStep("Resultaat");
|
||||
logInfo("Review geslaagd.");
|
||||
logInfo("Er zijn geen muterende acties uitgevoerd.");
|
||||
logInfo(`Controleer eventueel de screenshots in: ${options.outputDir}`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1007,21 +1341,43 @@ async function main() {
|
||||
throw new Error("Uitvoering geblokkeerd: markdown bevat bloktypes buiten de MVP-scope.");
|
||||
}
|
||||
|
||||
await executePlan(page, courseModel, options.outputDir);
|
||||
console.log("Uitvoering gestopt na reviewpunt. Er is niet opgeslagen.");
|
||||
await capture(page, options.outputDir, "review-stop");
|
||||
if (options.pauseAtEnd) {
|
||||
await waitForUserReview();
|
||||
logStep("Inhoud opbouwen");
|
||||
await executePlan(
|
||||
page,
|
||||
courseModel,
|
||||
options.outputDir,
|
||||
path.dirname(options.markdownPath)
|
||||
);
|
||||
|
||||
if (options.save) {
|
||||
logStep("Opslaan");
|
||||
logInfo("De inhoud is opgebouwd. De runner klikt nu op 'Opslaan als' en kiest daarna 'Concept'.");
|
||||
await clickSaveButton(page);
|
||||
await capture(page, options.outputDir, "after-save");
|
||||
logStep("Resultaat");
|
||||
logInfo("Execute-run afgerond.");
|
||||
logInfo("De runner heeft op 'Opslaan als' geklikt en daarna 'Concept' gekozen.");
|
||||
logInfo("Controleer in de browser of OnlineAcademy de save-actie bevestigt.");
|
||||
} else {
|
||||
await page.waitForTimeout(8000);
|
||||
logStep("Resultaat");
|
||||
logInfo("Execute-run afgerond tot het reviewpunt.");
|
||||
logInfo("De inhoud is opgebouwd in de editor.");
|
||||
logInfo("Er is nog niet opgeslagen.");
|
||||
logInfo("Het scherm blijft open zodat je kunt controleren wat er is ingevoerd.");
|
||||
await capture(page, options.outputDir, "review-stop");
|
||||
await waitForUserReview();
|
||||
}
|
||||
} catch (error) {
|
||||
await capture(page, options.outputDir, "failure-state").catch(() => {});
|
||||
throw error;
|
||||
throw new Error(buildFriendlyErrorMessage(error, options));
|
||||
}
|
||||
} finally {
|
||||
await browser.close();
|
||||
}
|
||||
}
|
||||
|
||||
await main();
|
||||
await main().catch((error) => {
|
||||
console.error(`\n[RESULTAAT] Mislukt`);
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user