Add batch picture candidate workflow
This commit is contained in:
@@ -167,8 +167,29 @@ There are three selectable data sources:
|
||||
- Picture candidates must show source, image dimensions and the proposed target assignment. The target assignment is not always color: supported strategies are `color`, `size`, `combination` and `manual`. Start by using manual corrections from the user, then learn reusable rules in Local scrape DB.
|
||||
- Scraped pictures and their metadata belong in Local scrape DB, not browser localStorage. Store the binary image in `scrape_image_blobs` with `sha256`, `perceptual_hash`, mime type, dimensions and size. Store each source/target proposal in `scrape_picture_candidates`.
|
||||
- Every picture candidate row must have its own save action. Saving one candidate stores only that selected image candidate into Local scrape DB and never saves the whole source result list.
|
||||
- Picture candidate popups must also provide `Save all` for robot/automatic flows. `Save all` saves every currently visible picture candidate one by one and must not save hidden, stale or unrelated candidates.
|
||||
- Source candidate popups should provide `Get all pictures` for robot/automatic flows. It fetches picture candidates from every currently visible mapped source, then opens the picture candidate popup. Saving happens only in the picture candidate popup.
|
||||
- Use `sha256` to remove exact duplicate image files and a perceptual hash to flag visually similar images from different sources or sizes. Exact duplicates may be collapsed automatically; visual duplicates should remain reviewable until the user approves the rule.
|
||||
- Product source cache is cleared when a new product loads.
|
||||
|
||||
### Picture Scraping Agent
|
||||
|
||||
- Treat picture scraping as a separate internal role: find sources, fetch picture candidates, classify target assignment, detect duplicates and prepare save actions.
|
||||
- The picture scraping agent stores all scraper settings, candidates, hashes, metadata and learned target rules in Local scrape DB (`catalog_scrape`).
|
||||
- Picture files are stored as binary blobs in Local scrape DB with disk-friendly metadata, not in browser localStorage and not in the live catalog database.
|
||||
- The agent starts with user-guided target assignment (`color`, `size`, `combination`, `manual`) and turns repeated corrections into reusable Local scrape DB rules.
|
||||
- The agent may compare Hudy, Idealo and future sources, but it must keep all candidates reviewable until an exact duplicate or learned rule is trusted.
|
||||
- The agent never writes to Live 9bplus DB. Live data may only be read later for comparison when explicitly allowed.
|
||||
|
||||
#### Picture Scraping Skills
|
||||
|
||||
- Source discovery: use manufacturer mapping from Local scrape DB and show exactly which source produced each candidate.
|
||||
- Browser scraping: use the configured browser engine per source and keep the UI responsive while scraping runs.
|
||||
- Image extraction: collect original image URLs, dimensions, mime type, file size and source page URL.
|
||||
- Duplicate detection: calculate `sha256` for exact duplicates and perceptual hash for visually similar images.
|
||||
- Target assignment: propose whether pictures belong to `color`, `size`, `combination` or `manual` target, then learn from user corrections.
|
||||
- Persistence: save picture blobs, candidates, hashes, source metadata and learned rules only into Local scrape DB.
|
||||
- Review UI: show picture size, source, target assignment, duplicate state, per-row save and batch save actions.
|
||||
- Hudy picture parsing uses the correct product color variant, checks EANs and extracts large gallery images.
|
||||
- Idealo uses the browser adapter and is intended mainly as a picture source.
|
||||
- Image preview is square.
|
||||
|
||||
+66
-4
@@ -34,6 +34,7 @@ const sidebarOverlay = document.querySelector("#sidebarOverlay");
|
||||
const sourceModal = document.querySelector("#sourceModal");
|
||||
const sourceTitle = document.querySelector("#sourceTitle");
|
||||
const sourceList = document.querySelector("#sourceList");
|
||||
const sourceBatchActionButton = document.querySelector("#sourceBatchActionButton");
|
||||
const sourceReloadButton = document.querySelector("#sourceReloadButton");
|
||||
const sourceCloseButton = document.querySelector("#sourceCloseButton");
|
||||
const settingsButton = document.querySelector("#settingsButton");
|
||||
@@ -200,6 +201,7 @@ loadProductButton.title = "Load existing product attribute info.";
|
||||
loadProductButton.addEventListener("click", loadProductInfoForCurrentProduct);
|
||||
findSourcesButton.addEventListener("click", findSourcesForCurrentProduct);
|
||||
sourceReloadButton.addEventListener("click", () => findSourcesForCurrentProduct(true));
|
||||
sourceBatchActionButton.addEventListener("click", handleSourceBatchAction);
|
||||
getPicturesButton.addEventListener("click", getPicturesForCurrentProduct);
|
||||
secondaryPanelToggle.addEventListener("click", toggleSecondaryPanel);
|
||||
mainWorkspaceToggle.addEventListener("click", toggleMainWorkspace);
|
||||
@@ -404,6 +406,23 @@ function setButtonLabel(button, label) {
|
||||
button.textContent = label;
|
||||
}
|
||||
}
|
||||
function setButtonIcon(button, iconName) {
|
||||
const useElement = button.querySelector(":scope > svg use");
|
||||
if (useElement)
|
||||
useElement.setAttribute("href", `/icons.svg#${iconName}`);
|
||||
}
|
||||
function setSourceBatchAction(mode) {
|
||||
sourceBatchActionButton.dataset.mode = mode;
|
||||
sourceBatchActionButton.hidden = false;
|
||||
sourceBatchActionButton.disabled = false;
|
||||
if (mode === "get-pictures") {
|
||||
setButtonIcon(sourceBatchActionButton, "image");
|
||||
setButtonLabel(sourceBatchActionButton, "Get all pictures");
|
||||
return;
|
||||
}
|
||||
setButtonIcon(sourceBatchActionButton, "save");
|
||||
setButtonLabel(sourceBatchActionButton, "Save all");
|
||||
}
|
||||
async function loadLanguages() {
|
||||
languageSelect.replaceChildren(createOption("", "Select language"));
|
||||
try {
|
||||
@@ -821,6 +840,7 @@ function getFirstSupplierColorGroup() {
|
||||
return [...groups.values()].find((group) => group.supplierRows.length > 0) || null;
|
||||
}
|
||||
function renderSourceCandidates(items) {
|
||||
sourceBatchActionButton.hidden = true;
|
||||
if (!items.length) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "source-row";
|
||||
@@ -828,6 +848,7 @@ function renderSourceCandidates(items) {
|
||||
sourceList.replaceChildren(empty);
|
||||
return;
|
||||
}
|
||||
setSourceBatchAction("get-pictures");
|
||||
sourceList.replaceChildren(...items.map((item) => {
|
||||
const section = document.createElement("section");
|
||||
section.className = "source-section";
|
||||
@@ -855,12 +876,14 @@ function renderSourceCandidates(items) {
|
||||
}));
|
||||
}
|
||||
function renderPictureCandidates(items) {
|
||||
sourceBatchActionButton.hidden = true;
|
||||
const rows = [];
|
||||
let position = 0;
|
||||
for (const item of items) {
|
||||
for (const candidate of item.candidates || []) {
|
||||
for (const picture of candidate.pictures || []) {
|
||||
position += 1;
|
||||
const sourcePosition = position;
|
||||
const target = buildPictureTargetInfo();
|
||||
const row = document.createElement("div");
|
||||
row.className = "picture-candidate";
|
||||
@@ -878,7 +901,7 @@ function renderPictureCandidates(items) {
|
||||
details.className = "picture-candidate-meta";
|
||||
const label = document.createElement("span");
|
||||
label.className = "picture-candidate-title";
|
||||
label.textContent = `${item.source?.name || item.source?.key || "Source"} picture #${position}`;
|
||||
label.textContent = `${item.source?.name || item.source?.key || "Source"} picture #${sourcePosition}`;
|
||||
const dimensions = document.createElement("span");
|
||||
dimensions.className = "picture-candidate-dimensions";
|
||||
dimensions.textContent = "Size: loading...";
|
||||
@@ -901,16 +924,17 @@ function renderPictureCandidates(items) {
|
||||
action.type = "button";
|
||||
action.className = "source-picture-action picture-candidate-save";
|
||||
action.textContent = "Save to DB";
|
||||
action.addEventListener("click", () => savePictureCandidateToDb({
|
||||
action.savePictureCandidate = () => savePictureCandidateToDb({
|
||||
picture,
|
||||
item,
|
||||
candidate,
|
||||
image,
|
||||
position,
|
||||
position: sourcePosition,
|
||||
target,
|
||||
action,
|
||||
duplicateHint,
|
||||
}));
|
||||
});
|
||||
action.addEventListener("click", () => action.savePictureCandidate());
|
||||
row.append(preview, details, action);
|
||||
rows.push(row);
|
||||
}
|
||||
@@ -923,6 +947,7 @@ function renderPictureCandidates(items) {
|
||||
sourceList.replaceChildren(empty);
|
||||
return;
|
||||
}
|
||||
setSourceBatchAction("save-pictures");
|
||||
sourceList.replaceChildren(...rows);
|
||||
}
|
||||
function buildPictureTargetInfo() {
|
||||
@@ -990,16 +1015,53 @@ async function savePictureCandidateToDb({ picture, item, candidate, image, posit
|
||||
if (!response.ok)
|
||||
throw new Error(data.message || "Picture save failed.");
|
||||
action.textContent = "Saved";
|
||||
action.dataset.saved = "1";
|
||||
duplicateHint.textContent = data.duplicateStatus === "exact_duplicate"
|
||||
? `DB: duplicate of #${data.duplicateOfCandidateId || "known"}`
|
||||
: `DB: saved #${data.candidateId}`;
|
||||
return true;
|
||||
}
|
||||
catch (error) {
|
||||
action.disabled = false;
|
||||
action.textContent = "Save to DB";
|
||||
duplicateHint.textContent = `DB: ${error.message || "save failed"}`;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
async function saveAllVisiblePictureCandidates() {
|
||||
const buttons = [...sourceList.querySelectorAll(".picture-candidate-save")]
|
||||
.filter((button) => button.savePictureCandidate && button.dataset.saved !== "1");
|
||||
if (!buttons.length)
|
||||
return;
|
||||
sourceBatchActionButton.disabled = true;
|
||||
setButtonLabel(sourceBatchActionButton, `Saving 0/${buttons.length}`);
|
||||
let savedCount = 0;
|
||||
for (const button of buttons) {
|
||||
const saved = await button.savePictureCandidate();
|
||||
if (saved)
|
||||
savedCount += 1;
|
||||
setButtonLabel(sourceBatchActionButton, `Saving ${savedCount}/${buttons.length}`);
|
||||
}
|
||||
sourceBatchActionButton.disabled = false;
|
||||
setButtonLabel(sourceBatchActionButton, savedCount === buttons.length ? "Saved all" : `Saved ${savedCount}/${buttons.length}`);
|
||||
}
|
||||
async function handleSourceBatchAction() {
|
||||
if (sourceBatchActionButton.dataset.mode === "get-pictures") {
|
||||
sourceBatchActionButton.disabled = true;
|
||||
setButtonLabel(sourceBatchActionButton, "Loading pictures...");
|
||||
try {
|
||||
await getPicturesForCurrentProduct("");
|
||||
}
|
||||
finally {
|
||||
sourceBatchActionButton.disabled = false;
|
||||
if (sourceBatchActionButton.dataset.mode === "get-pictures") {
|
||||
setButtonLabel(sourceBatchActionButton, "Get all pictures");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
await saveAllVisiblePictureCandidates();
|
||||
}
|
||||
function collectPictureUrls(items) {
|
||||
return items.flatMap((item) => (item.candidates || []).flatMap((candidate) => candidate.pictures || [])).filter((url, index, all) => all.indexOf(url) === index);
|
||||
}
|
||||
|
||||
+74
-5
@@ -34,6 +34,7 @@ const sidebarOverlay = document.querySelector("#sidebarOverlay");
|
||||
const sourceModal = document.querySelector("#sourceModal");
|
||||
const sourceTitle = document.querySelector("#sourceTitle");
|
||||
const sourceList = document.querySelector("#sourceList");
|
||||
const sourceBatchActionButton = document.querySelector("#sourceBatchActionButton");
|
||||
const sourceReloadButton = document.querySelector("#sourceReloadButton");
|
||||
const sourceCloseButton = document.querySelector("#sourceCloseButton");
|
||||
const settingsButton = document.querySelector("#settingsButton");
|
||||
@@ -205,6 +206,7 @@ loadProductButton.title = "Load existing product attribute info.";
|
||||
loadProductButton.addEventListener("click", loadProductInfoForCurrentProduct);
|
||||
findSourcesButton.addEventListener("click", findSourcesForCurrentProduct);
|
||||
sourceReloadButton.addEventListener("click", () => findSourcesForCurrentProduct(true));
|
||||
sourceBatchActionButton.addEventListener("click", handleSourceBatchAction);
|
||||
getPicturesButton.addEventListener("click", getPicturesForCurrentProduct);
|
||||
secondaryPanelToggle.addEventListener("click", toggleSecondaryPanel);
|
||||
mainWorkspaceToggle.addEventListener("click", toggleMainWorkspace);
|
||||
@@ -425,6 +427,25 @@ function setButtonLabel(button, label) {
|
||||
}
|
||||
}
|
||||
|
||||
function setButtonIcon(button, iconName) {
|
||||
const useElement = button.querySelector(":scope > svg use");
|
||||
if (useElement) useElement.setAttribute("href", `/icons.svg#${iconName}`);
|
||||
}
|
||||
|
||||
function setSourceBatchAction(mode) {
|
||||
sourceBatchActionButton.dataset.mode = mode;
|
||||
sourceBatchActionButton.hidden = false;
|
||||
sourceBatchActionButton.disabled = false;
|
||||
if (mode === "get-pictures") {
|
||||
setButtonIcon(sourceBatchActionButton, "image");
|
||||
setButtonLabel(sourceBatchActionButton, "Get all pictures");
|
||||
return;
|
||||
}
|
||||
|
||||
setButtonIcon(sourceBatchActionButton, "save");
|
||||
setButtonLabel(sourceBatchActionButton, "Save all");
|
||||
}
|
||||
|
||||
async function loadLanguages() {
|
||||
languageSelect.replaceChildren(createOption("", "Select language"));
|
||||
try {
|
||||
@@ -880,6 +901,7 @@ function getFirstSupplierColorGroup() {
|
||||
}
|
||||
|
||||
function renderSourceCandidates(items) {
|
||||
sourceBatchActionButton.hidden = true;
|
||||
if (!items.length) {
|
||||
const empty = document.createElement("div");
|
||||
empty.className = "source-row";
|
||||
@@ -888,6 +910,7 @@ function renderSourceCandidates(items) {
|
||||
return;
|
||||
}
|
||||
|
||||
setSourceBatchAction("get-pictures");
|
||||
sourceList.replaceChildren(
|
||||
...items.map((item) => {
|
||||
const section = document.createElement("section");
|
||||
@@ -927,12 +950,14 @@ function renderSourceCandidates(items) {
|
||||
}
|
||||
|
||||
function renderPictureCandidates(items) {
|
||||
sourceBatchActionButton.hidden = true;
|
||||
const rows = [];
|
||||
let position = 0;
|
||||
for (const item of items) {
|
||||
for (const candidate of item.candidates || []) {
|
||||
for (const picture of candidate.pictures || []) {
|
||||
position += 1;
|
||||
const sourcePosition = position;
|
||||
const target = buildPictureTargetInfo();
|
||||
const row = document.createElement("div");
|
||||
row.className = "picture-candidate";
|
||||
@@ -954,7 +979,7 @@ function renderPictureCandidates(items) {
|
||||
|
||||
const label = document.createElement("span");
|
||||
label.className = "picture-candidate-title";
|
||||
label.textContent = `${item.source?.name || item.source?.key || "Source"} picture #${position}`;
|
||||
label.textContent = `${item.source?.name || item.source?.key || "Source"} picture #${sourcePosition}`;
|
||||
|
||||
const dimensions = document.createElement("span");
|
||||
dimensions.className = "picture-candidate-dimensions";
|
||||
@@ -982,18 +1007,18 @@ function renderPictureCandidates(items) {
|
||||
action.type = "button";
|
||||
action.className = "source-picture-action picture-candidate-save";
|
||||
action.textContent = "Save to DB";
|
||||
action.addEventListener("click", () =>
|
||||
action.savePictureCandidate = () =>
|
||||
savePictureCandidateToDb({
|
||||
picture,
|
||||
item,
|
||||
candidate,
|
||||
image,
|
||||
position,
|
||||
position: sourcePosition,
|
||||
target,
|
||||
action,
|
||||
duplicateHint,
|
||||
}),
|
||||
);
|
||||
});
|
||||
action.addEventListener("click", () => action.savePictureCandidate());
|
||||
|
||||
row.append(preview, details, action);
|
||||
rows.push(row);
|
||||
@@ -1008,6 +1033,7 @@ function renderPictureCandidates(items) {
|
||||
sourceList.replaceChildren(empty);
|
||||
return;
|
||||
}
|
||||
setSourceBatchAction("save-pictures");
|
||||
sourceList.replaceChildren(...rows);
|
||||
}
|
||||
|
||||
@@ -1089,16 +1115,59 @@ async function savePictureCandidateToDb({
|
||||
if (!response.ok) throw new Error(data.message || "Picture save failed.");
|
||||
|
||||
action.textContent = "Saved";
|
||||
action.dataset.saved = "1";
|
||||
duplicateHint.textContent = data.duplicateStatus === "exact_duplicate"
|
||||
? `DB: duplicate of #${data.duplicateOfCandidateId || "known"}`
|
||||
: `DB: saved #${data.candidateId}`;
|
||||
return true;
|
||||
} catch (error) {
|
||||
action.disabled = false;
|
||||
action.textContent = "Save to DB";
|
||||
duplicateHint.textContent = `DB: ${error.message || "save failed"}`;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function saveAllVisiblePictureCandidates() {
|
||||
const buttons = [...sourceList.querySelectorAll(".picture-candidate-save")]
|
||||
.filter((button) => button.savePictureCandidate && button.dataset.saved !== "1");
|
||||
|
||||
if (!buttons.length) return;
|
||||
|
||||
sourceBatchActionButton.disabled = true;
|
||||
setButtonLabel(sourceBatchActionButton, `Saving 0/${buttons.length}`);
|
||||
let savedCount = 0;
|
||||
for (const button of buttons) {
|
||||
const saved = await button.savePictureCandidate();
|
||||
if (saved) savedCount += 1;
|
||||
setButtonLabel(sourceBatchActionButton, `Saving ${savedCount}/${buttons.length}`);
|
||||
}
|
||||
|
||||
sourceBatchActionButton.disabled = false;
|
||||
setButtonLabel(
|
||||
sourceBatchActionButton,
|
||||
savedCount === buttons.length ? "Saved all" : `Saved ${savedCount}/${buttons.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
async function handleSourceBatchAction() {
|
||||
if (sourceBatchActionButton.dataset.mode === "get-pictures") {
|
||||
sourceBatchActionButton.disabled = true;
|
||||
setButtonLabel(sourceBatchActionButton, "Loading pictures...");
|
||||
try {
|
||||
await getPicturesForCurrentProduct("");
|
||||
} finally {
|
||||
sourceBatchActionButton.disabled = false;
|
||||
if (sourceBatchActionButton.dataset.mode === "get-pictures") {
|
||||
setButtonLabel(sourceBatchActionButton, "Get all pictures");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
await saveAllVisiblePictureCandidates();
|
||||
}
|
||||
|
||||
function collectPictureUrls(items) {
|
||||
return items.flatMap((item) =>
|
||||
(item.candidates || []).flatMap((candidate) => candidate.pictures || []),
|
||||
|
||||
@@ -147,6 +147,7 @@
|
||||
<header class="suggested-header">
|
||||
<h2 id="sourceTitle">Source candidates</h2>
|
||||
<div class="source-dialog-actions">
|
||||
<button class="modal-close" id="sourceBatchActionButton" type="button" hidden><svg class="sidebar-icon" aria-hidden="true"><use href="/icons.svg#image"></use></svg><span>Get all pictures</span></button>
|
||||
<button class="modal-close" id="sourceReloadButton" type="button"><svg class="sidebar-icon" aria-hidden="true"><use href="/icons.svg#refresh"></use></svg><span>Reload</span></button>
|
||||
<button class="modal-close" id="sourceCloseButton" type="button"><svg class="sidebar-icon" aria-hidden="true"><use href="/icons.svg#close"></use></svg><span>Close</span></button>
|
||||
</div>
|
||||
|
||||
@@ -192,6 +192,127 @@ test("picture candidates show source, dimensions, target assignment and save act
|
||||
{
|
||||
source: { key: "hudy", name: "Hudy", type: "supplier" },
|
||||
searchUrl: "https://www.hudy.cz/vyhledavani?q=8058428191284",
|
||||
candidates: [
|
||||
{
|
||||
rawTitle: "Aequilibrium Trek Woman GTX",
|
||||
url: "https://www.hudy.cz/product",
|
||||
pictures: [
|
||||
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='80' height='64'%3E%3Crect width='80' height='64' fill='red'/%3E%3C/svg%3E",
|
||||
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='90'%3E%3Crect width='120' height='90' fill='blue'/%3E%3C/svg%3E",
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
});
|
||||
let saveRequests = 0;
|
||||
await page.route("**/api/catalog-maker/save-picture", async (route) => {
|
||||
saveRequests += 1;
|
||||
await route.fulfill({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
saved: true,
|
||||
candidateId: saveRequests,
|
||||
duplicateStatus: "unique",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
await page.addInitScript(() => {
|
||||
localStorage.setItem("catalog-maker:selected-manufacturer", "13");
|
||||
});
|
||||
await page.goto("/");
|
||||
await page.waitForFunction(() => document.querySelector("#manufacturerSelect")?.value === "13");
|
||||
await page.locator("#getPicturesButton").click();
|
||||
|
||||
await expect(page.locator("#sourceModal")).toBeVisible();
|
||||
await expect(page.locator(".picture-candidate-title").first()).toHaveText("Hudy picture #1");
|
||||
await expect(page.locator(".picture-candidate-dimensions").first()).toContainText("Size:");
|
||||
await expect(page.locator(".picture-candidate-target").first()).toContainText("Target: color Carbon/Malibu Blue");
|
||||
await expect(page.locator(".picture-candidate-save")).toHaveCount(2);
|
||||
await expect(page.locator("#sourceBatchActionButton")).toBeVisible();
|
||||
await expect(page.locator("#sourceBatchActionButton")).toContainText("Save all");
|
||||
|
||||
await page.locator("#sourceBatchActionButton").click();
|
||||
await expect(page.locator(".picture-candidate-save")).toHaveText(["Saved", "Saved"]);
|
||||
expect(saveRequests).toBe(2);
|
||||
});
|
||||
|
||||
test("source candidates can fetch all pictures before saving candidates", async ({ page }) => {
|
||||
await page.route("**/api/manufacturers", async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ items: [{ id: 13, name: "La Sportiva" }] }),
|
||||
});
|
||||
});
|
||||
await page.route("**/api/languages", async (route) => {
|
||||
await route.fulfill({ contentType: "application/json", body: JSON.stringify({ items: [] }) });
|
||||
});
|
||||
await page.route("**/api/catalog-maker/mapping-sources**", async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
mapped: true,
|
||||
items: [{ key: "hudy", name: "Hudy", enabled: true, health: "ok" }],
|
||||
}),
|
||||
});
|
||||
});
|
||||
await page.route("**/api/catalog-maker/product**", async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
product: {
|
||||
idProductCatalog: 135520,
|
||||
idProduct: 110927,
|
||||
idManufacturer: 13,
|
||||
codeManufacture: "LAS",
|
||||
supplierReference: "ZFHS099",
|
||||
name: "Aequilibrium Trek Woman GTX",
|
||||
position: 1,
|
||||
total: 380,
|
||||
},
|
||||
combinations: [
|
||||
{
|
||||
idProductCatalog: 110923,
|
||||
status: "SS25/Boty",
|
||||
color: "Carbon/Malibu Blue",
|
||||
combination: "36",
|
||||
ean13: "8058428191284",
|
||||
supplierStock: 1,
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
});
|
||||
await page.route("**/api/catalog-maker/find-sources**", async (route) => {
|
||||
await route.fulfill({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
items: [
|
||||
{
|
||||
status: "found",
|
||||
source: { key: "hudy", name: "Hudy", type: "supplier" },
|
||||
candidates: [
|
||||
{
|
||||
title: "Direct Hudy / EAN: 8058428191284 / Color: Carbon/Malibu Blue",
|
||||
url: "https://www.hudy.cz/product",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
});
|
||||
await page.route("**/api/catalog-maker/get-pictures**", async (route) => {
|
||||
expect(route.request().url()).not.toContain("sourceKey=");
|
||||
await route.fulfill({
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
items: [
|
||||
{
|
||||
source: { key: "hudy", name: "Hudy", type: "supplier" },
|
||||
candidates: [
|
||||
{
|
||||
rawTitle: "Aequilibrium Trek Woman GTX",
|
||||
@@ -212,13 +333,14 @@ test("picture candidates show source, dimensions, target assignment and save act
|
||||
});
|
||||
await page.goto("/");
|
||||
await page.waitForFunction(() => document.querySelector("#manufacturerSelect")?.value === "13");
|
||||
await page.locator("#getPicturesButton").click();
|
||||
await page.locator("#findSourcesButton").click();
|
||||
|
||||
await expect(page.locator("#sourceModal")).toBeVisible();
|
||||
await expect(page.locator(".picture-candidate-title")).toHaveText("Hudy picture #1");
|
||||
await expect(page.locator(".picture-candidate-dimensions")).toContainText("Size:");
|
||||
await expect(page.locator(".picture-candidate-target")).toContainText("Target: color Carbon/Malibu Blue");
|
||||
await expect(page.locator(".picture-candidate-save")).toHaveText("Save to DB");
|
||||
await expect(page.locator("#sourceBatchActionButton")).toBeVisible();
|
||||
await expect(page.locator("#sourceBatchActionButton")).toContainText("Get all pictures");
|
||||
await page.locator("#sourceBatchActionButton").click();
|
||||
await expect(page.locator(".picture-candidate-title").first()).toHaveText("Hudy picture #1");
|
||||
await expect(page.locator("#sourceBatchActionButton")).toContainText("Save all");
|
||||
});
|
||||
|
||||
test("uses one shared height for the top panel headers", async ({ page }) => {
|
||||
|
||||
Reference in New Issue
Block a user