begin setup automation with deno and playwright

This commit is contained in:
2024-11-15 16:06:32 +01:00
parent 5c08cc9cfd
commit 9d4d33ef45
19 changed files with 705 additions and 1 deletions

View File

@@ -0,0 +1,97 @@
/*
docker run -v ./script.js:/app/script.js --rm playwright:1.47.0
*/
import type { Page } from "playwright";
import forms from "../forms.ts";
export type CompanyInfo = {
raisonSociale: string;
adresse: string;
codePostal: string;
ville: string;
pays: string; // France (FR)
telephoneFixe?: string;
telephonePortable?: string;
email: string;
siteWeb: string;
logoFilePath: string;
};
export type CompanyID = {
directeurs: string; // Nom du(des) gestionnaire(s) (PDG, directeur, président...)
responsableRGPD: string; // Délégué à la protection des données (DPO, contact RGPD, ...)
capital: string; // Capital
formeJuridique: string; // Type d'entité légale (SASU,...)
siren: string; // SIREN
siret: string; // SIRET
naf_ape: string; // NAF-APE
rcs_rm: string; // RCS/RM (RNE)
numEori?: string; // numéro EORI (douanes)
numRna?: string; // numéro RNA (associations)
numTva: string; // Numéro de TVA
objetDeLaSociete: string; // Objet de la société
moisDebutExercice: string; // Mois de début d'exercice
};
export type Company = {
info: CompanyInfo;
ID: CompanyID;
};
const companyInfoDolibarrInputNames = new Map(Object.entries({
raisonSociale: "name",
adresse: "MAIN_INFO_SOCIETE_ADDRESS",
codePostal: "MAIN_INFO_SOCIETE_ZIP",
ville: "MAIN_INFO_SOCIETE_TOWN",
pays: "country_id:select",
telephoneFixe: "phone",
telephonePortable: "phone_mobile",
email: "mail",
siteWeb: "web",
logoFilePath: "logo_squarred:file",
})) as Map<keyof CompanyInfo, string>;
const companyIDDolibarrInputNames = new Map(Object.entries({
directeurs: "MAIN_INFO_SOCIETE_MANAGERS",
responsableRGPD: "MAIN_INFO_GDPR",
capital: "capital",
formeJuridique: "forme_juridique_code:select",
siren: "siren",
siret: "siret",
naf_ape: "ape",
rcs_rm: "rcs",
numEori: "MAIN_INFO_PROFID5",
numRna: "MAIN_INFO_PROFID6",
numTva: "tva",
objetDeLaSociete: "socialobject",
moisDebutExercice: "SOCIETE_FISCAL_MONTH_START:select",
})) as Map<keyof CompanyID, string>;
async function setupCompany(
{ imgFolderPath, page, dolibarrAddress }: {
imgFolderPath: string;
page: Page;
dolibarrAddress: string;
},
company: Company,
): Promise<void> {
await page.goto(`${dolibarrAddress}/admin/company.php`);
await forms.fillForm(
{ page, imgFolderPath },
company.info,
companyInfoDolibarrInputNames,
1,
);
await forms.fillForm(
{ page, imgFolderPath },
company.ID,
companyIDDolibarrInputNames,
2,
);
}
export default {
setupCompany,
};

View File

@@ -0,0 +1,94 @@
/*
docker run -v ./script.js:/app/script.js --rm playwright:1.47.0
*/
import type { Page } from "playwright";
import login from "../login.ts";
import forms from "../forms.ts";
async function removeLoginForgottenPasswordLink(
{ page, dolibarrAddress }: {
page: Page;
dolibarrAddress: string;
imgFolderPath: string;
},
): Promise<void> {
await page.goto(`${dolibarrAddress}/admin/security.php`);
const label = `Cacher le lien "Mot de passe oublié" sur la page de connexion`;
const row = page.locator("tr").filter({
has: page.locator("td", { hasText: label }),
});
const enableLink = row.locator("a", {
hasText: /^Activer$/,
hasNotText: /^Désactiver$/,
});
const disableLink = row.locator("a", {
hasText: /^Désactiver$/,
hasNotText: /^Activer$/,
});
if (await enableLink.isVisible()) {
await enableLink.click();
} else if (!await disableLink.isVisible()) {
throw new Error("Enable/Disable link not found");
}
}
async function setupLoginPage(
globalCtx: {
page: Page;
dolibarrAddress: string;
imgFolderPath: string;
},
{ removeHelpLink, HTMLMessage, backgroundImage }: {
removeHelpLink?: boolean;
HTMLMessage?: string;
backgroundImage?: string;
},
): Promise<void> {
const { page, dolibarrAddress } = globalCtx;
await page.goto(`${dolibarrAddress}/admin/ihm.php?mode=login`);
await forms.fillForm(
globalCtx,
{
HTMLMessage,
backgroundImage,
removeHelpLink: (removeHelpLink||false).toString(),
},
new Map([
["removeHelpLink",`Cacher le lien «Besoin d'aide ou assistance» sur la page de connexion:toggle`],
["backgroundImage","imagebackground:file"],
["HTMLMessage", "main_home:cke"]
]),
1,
);
}
async function setupDisplay(
globalCtx: {
page: Page;
dolibarrAddress: string;
adminCredentials: { username: string; password: string };
imgFolderPath: string;
},
): Promise<void> {
await login.doAdminLogin(globalCtx);
await removeLoginForgottenPasswordLink(globalCtx);
await setupLoginPage(globalCtx, {
removeHelpLink: true,
HTMLMessage: `
<div style="text-align:center"><span style="color:#000000">Bienvenue chez </span><span style="color:#2c3e50"><em>Gabriel Radureau</em></span><br />
<span style="color:#000000"><em>l&#39;</em></span><span style="color:#e74c3c"><em>ar</em></span><span style="color:#c0392b"><em>change</em></span><span style="color:#000000"><em> du </em></span><span style="color:#2ecc71"><em>code</em></span><br />
<span style="color:#000000">🏹💻🪽<br />
<em><span style="font-size:8px">Pariez sur le bon sagitaire</span></em></span></div>
`.trim(),
backgroundImage: "$IMG/loginBackground.jpeg",
});
}
export default {
setupDisplay,
removeLoginForgottenPasswordLink,
setupLoginPage,
};

View File

@@ -0,0 +1,63 @@
/*
docker run -v ./script.js:/app/script.js --rm playwright:1.47.0
*/
import type { Page } from "playwright";
interface credentials {
username: string;
password: string;
}
async function goToInstallationPage(
{ page, dolibarrAddress }: { page: Page; dolibarrAddress: string },
): Promise<void> {
await page.goto(`${dolibarrAddress}/install/`);
}
async function doFirstInstall(
{ page, dolibarrAddress, DBpassword, adminCredentials }: {
page: Page;
dolibarrAddress: string;
DBpassword: string;
adminCredentials: credentials;
},
): Promise<void> {
await goToInstallationPage({ page, dolibarrAddress });
const beginFirstInstallBtn = page.locator(
"table table table tr:last-of-type a.button",
);
await beginFirstInstallBtn.click();
// 1
await page.fill('input[name="db_pass"]', DBpassword);
await page.click('input[type="submit"]');
// 2 Migrations
await page.click('input[type="submit"]', { timeout: 3600 * 1000 });
// 3
await page.uncheck('input[name="dolibarrpingno"]');
await page.click('input[type="submit"]');
await page.fill('input[name="login"]', adminCredentials.username);
await page.fill('input[name="pass"]', adminCredentials.password);
await page.fill('input[name="pass_verif"]', adminCredentials.password);
await page.click('input[type="submit"]');
}
async function isUpgradeLocked(
{ page, dolibarrAddress }: { page: Page; dolibarrAddress: string },
): Promise<boolean> {
await goToInstallationPage({ page, dolibarrAddress });
return (
await page.locator("table").count() === 0 &&
await page.getByText("Cliquez ici pour aller sur votre application")
.count() > 0
);
}
export default {
doFirstInstall,
isUpgradeLocked,
};

75
test/scripts/forms.ts Normal file
View File

@@ -0,0 +1,75 @@
import type { Page } from "playwright";
async function toggleOnOff({page}:{page:Page}, label: string, on: boolean) : Promise<void>{
const row = page.locator("tr").filter({
has: page.locator("td", { hasText: label }),
});
const hideBtn = row.locator(`[title="Désactivé"]`);
const showBtn = row.locator(`[title="Actif"]`);
if (!await hideBtn.isVisible() && !await showBtn.isVisible()) {
throw new Error("Show/Hide button not found");
}
if (on && await hideBtn.isVisible()) await hideBtn.click();
if (!on && await showBtn.isVisible()) await showBtn.click();
}
// Fonction générique `fillForm` pour remplir des formulaires
async function fillForm<T>(
{ page, imgFolderPath }: { page: Page; imgFolderPath: string },
data: T,
inputNames: Map<keyof T, string>,
submit: number = 0,
) {
for (const [attr, input] of inputNames) {
const attrValue = data[attr];
if (!attrValue) continue;
const [inputName, inputType] = input.split(":");
console.log(`${inputName} => '${attrValue}'`);
switch (inputType) {
case "select":
await page.selectOption(`select[name="${inputName}"]`, {
label: attrValue as string,
});
break;
case "file": {
const [fileChooser] = await Promise.all([
page.waitForEvent("filechooser"),
page.click(`input[name="${inputName}"]`),
]);
await fileChooser.setFiles(
(attrValue as string).replace("$IMG", imgFolderPath),
);
break;
}
case "cke": { // CKEditor https://ckeditor.com/wysiwyg-editor-open-source/
const col = page.locator("td", {has: page.locator(`textarea[name="${inputName}"]`)});
const htmlEditor = col.frameLocator('iframe').locator('body');
const rawHTMLEditor = col.locator('textarea.cke_editable');
const htmlToggleBtn = col.getByRole("button").filter({hasText: /Source/});
await htmlToggleBtn.click();
await rawHTMLEditor.fill(attrValue as string);
await htmlToggleBtn.click();
await htmlEditor.screenshot({path: 'save.png'});
break;
}
case "toggle":
await toggleOnOff({page}, inputName, Boolean(JSON.parse(attrValue as string)))
break;
default:
await page.fill(
`input[name="${inputName}"],textarea[name="${inputName}"]`,
attrValue as string,
);
}
}
if (submit > 0) {
await page.click(`:nth-match(input[type="submit"], ${submit})`);
}
}
export default {
fillForm,
};

64
test/scripts/login.ts Normal file
View File

@@ -0,0 +1,64 @@
import type { Page } from "playwright";
interface credentials {
username: string;
password: string;
}
async function whoAmI({ page }: {
page: Page;
}): Promise<string> {
if (await isLogOut({ page })) return "";
return (await page.locator('.login_block_user a[data-toggle="dropdown"]')
.textContent())?.trim() || "";
}
async function isLogOut({ page }: {
page: Page;
}): Promise<boolean> {
return (
await page.locator(".login_block").count() === 0 &&
await page.locator(".login_table").count() > 0
);
}
async function doLogout({ page }: {
page: Page;
}): Promise<void> {
if (await isLogOut({ page })) return;
await page.locator(".login_block .dropdown").click();
await page.locator("i.fa-sign-out-alt").click();
}
async function doLogin(
{ page, dolibarrAddress }: {
page: Page;
dolibarrAddress: string;
},
{ username, password }: credentials,
): Promise<void> {
await page.goto(`${dolibarrAddress}/`);
if (await whoAmI({ page }) === username) return;
await doLogout({ page });
await page.fill('input[name="username"]', username);
await page.fill('input[name="password"]', password);
await page.click('input[type="submit"]');
}
async function doAdminLogin({ page, dolibarrAddress, adminCredentials }: {
page: Page;
dolibarrAddress: string;
adminCredentials: credentials;
}) {
await doLogin({ page, dolibarrAddress }, adminCredentials);
}
export default {
doAdminLogin,
doLogin,
doLogout,
isLogOut,
whoAmI,
};