UNPKG

@shopware-ag/acceptance-test-suite

Version:
1,364 lines (1,350 loc) 352 kB
import { test as test$f, expect, request, mergeTests } from '@playwright/test'; export * from '@playwright/test'; import { Image } from 'image-js'; import crypto from 'crypto'; import { stringify } from 'uuid'; import { satisfies } from 'compare-versions'; import fs from 'fs'; import { AxeBuilder } from '@axe-core/playwright'; import { createHtmlReport } from 'axe-html-reporter'; const test$d = test$f.extend({ SalesChannelBaseConfig: [ async ({ Country, Currency, Language, PaymentMethod, ShippingMethod, SnippetSet, Tax, Theme }, use) => { await use({ enGBLocaleId: Language.translationCode.id, enGBLanguageId: Language.id, storefrontTypeId: "8a243080f92e4c719546314b577cf82b", eurCurrencyId: Currency.id, defaultCurrencyId: "b7d2554b0ce847cd82f3ac9bd1c0dfca", defaultLanguageId: "2fbb5fe2e29a4d70aa5854ce7ce3e20b", invoicePaymentMethodId: PaymentMethod.id, defaultShippingMethod: ShippingMethod.id, taxId: Tax.id, deCountryId: Country.id, enGBSnippetSetId: SnippetSet.id, defaultThemeId: Theme.id, appUrl: process.env["APP_URL"], adminUrl: process.env["ADMIN_URL"] || `${process.env["APP_URL"]}admin/` }); }, { scope: "worker" } ], DefaultSalesChannel: [ async ({ IdProvider, AdminApiContext, SalesChannelBaseConfig }, use) => { const { id, uuid } = IdProvider.getWorkerDerivedStableId("salesChannel"); const { uuid: rootCategoryUuid } = IdProvider.getWorkerDerivedStableId("category"); const { uuid: customerGroupUuid } = IdProvider.getWorkerDerivedStableId("customerGroup"); const { uuid: domainUuid } = IdProvider.getWorkerDerivedStableId("domain"); const { uuid: customerUuid } = IdProvider.getWorkerDerivedStableId("customer"); const baseUrl = `${SalesChannelBaseConfig.appUrl}test-${uuid}/`; await AdminApiContext.delete(`./customer/${customerUuid}`); const wantedLanguages = /* @__PURE__ */ new Set([SalesChannelBaseConfig.enGBLanguageId, SalesChannelBaseConfig.defaultLanguageId]); const languages = []; const result = await AdminApiContext.get(`./sales-channel/${uuid}/languages`); if (result.ok()) { const salesChannelLanguages = await result.json(); wantedLanguages.forEach((l) => { if (!salesChannelLanguages.data.find((i) => i.id === l)) { languages.push({ id: l }); } }); } else { wantedLanguages.forEach((l) => { languages.push({ id: l }); }); } const syncResp = await AdminApiContext.post("./_action/sync", { data: { "write-sales-channel": { entity: "sales_channel", action: "upsert", payload: [ { id: uuid, name: `${id} acceptance test`, typeId: SalesChannelBaseConfig.storefrontTypeId, languageId: SalesChannelBaseConfig.enGBLanguageId, currencyId: SalesChannelBaseConfig.eurCurrencyId, paymentMethodId: SalesChannelBaseConfig.invoicePaymentMethodId, shippingMethodId: SalesChannelBaseConfig.defaultShippingMethod, countryId: SalesChannelBaseConfig.deCountryId, accessKey: "SWSC" + uuid, homeEnabled: true, navigationCategory: { id: rootCategoryUuid, name: `${id} Acceptance test`, displayNestedProducts: true, type: "page", productAssignmentType: "product" }, domains: [{ id: domainUuid, url: baseUrl, languageId: SalesChannelBaseConfig.enGBLanguageId, snippetSetId: SalesChannelBaseConfig.enGBSnippetSetId, currencyId: SalesChannelBaseConfig.eurCurrencyId }], customerGroup: { id: customerGroupUuid, name: `${id} Acceptance test` }, languages, countries: [{ id: SalesChannelBaseConfig.deCountryId }], shippingMethods: [{ id: SalesChannelBaseConfig.defaultShippingMethod }], paymentMethods: [{ id: SalesChannelBaseConfig.invoicePaymentMethodId }], currencies: [{ id: SalesChannelBaseConfig.eurCurrencyId }] } ] } } }); expect(syncResp.ok()).toBeTruthy(); const salesChannelPromise = AdminApiContext.get(`./sales-channel/${uuid}`); const salutationResponse = await AdminApiContext.get(`./salutation`); const salutations = await salutationResponse.json(); const customerData = { id: customerUuid, email: `customer_${id}@example.com`, password: "shopware", salutationId: salutations.data[0].id, languageId: SalesChannelBaseConfig.enGBLanguageId, defaultShippingAddress: { firstName: `${id} admin`, lastName: `${id} admin`, city: "not", street: "not", zipcode: "not", countryId: SalesChannelBaseConfig.deCountryId, salutationId: salutations.data[0].id }, defaultBillingAddress: { firstName: `${id} admin`, lastName: `${id} admin`, city: "not", street: "not", zipcode: "not", countryId: SalesChannelBaseConfig.deCountryId, salutationId: salutations.data[0].id }, firstName: `${id} admin`, lastName: `${id} admin`, salesChannelId: uuid, groupId: customerGroupUuid, customerNumber: `${customerUuid}`, defaultPaymentMethodId: SalesChannelBaseConfig.invoicePaymentMethodId }; const customerRespPromise = AdminApiContext.post("./customer?_response", { data: customerData }); const [customerResp, salesChannelResp] = await Promise.all([ customerRespPromise, salesChannelPromise ]); expect(customerResp.ok()).toBeTruthy(); expect(salesChannelResp.ok()).toBeTruthy(); const customer = await customerResp.json(); const salesChannel = await salesChannelResp.json(); await use({ salesChannel: salesChannel.data, customer: { ...customer.data, password: customerData.password }, url: baseUrl }); }, { scope: "worker" } ] }); var __defProp$11 = Object.defineProperty; var __defNormalProp$11 = (obj, key, value) => key in obj ? __defProp$11(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField$11 = (obj, key, value) => { __defNormalProp$11(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; const _AdminApiContext = class _AdminApiContext { constructor(context, options) { __publicField$11(this, "context"); __publicField$11(this, "options"); this.context = context; this.options = options; } static async create(options) { const contextOptions = { ...this.defaultOptions, ...options }; const tmpContext = await this.createApiRequestContext(contextOptions); if (!contextOptions.client_id) { contextOptions["access_token"] = await this.authenticateWithUserPassword(tmpContext, contextOptions); const userContext = await this.createApiRequestContext(contextOptions); const accessKeyData = await (await userContext.get("_action/access-key/intergration")).json(); const integrationData = { admin: true, label: "Playwright Acceptance Test Suite", ...accessKeyData }; await userContext.post("integration", { data: integrationData }); contextOptions.client_id = accessKeyData.accessKey; contextOptions.client_secret = accessKeyData.secretAccessKey; } contextOptions["access_token"] = await this.authenticateWithClientCredentials(tmpContext, contextOptions); return new _AdminApiContext(await this.createApiRequestContext(contextOptions), contextOptions); } static async createApiRequestContext(options) { const extraHTTPHeaders = { "Accept": "application/json", "Content-Type": "application/json" }; if (options.access_token && options.access_token.length) { extraHTTPHeaders["Authorization"] = "Bearer " + options.access_token; } return await request.newContext({ baseURL: `${options.app_url}api/`, ignoreHTTPSErrors: options.ignoreHTTPSErrors, extraHTTPHeaders }); } static async authenticateWithClientCredentials(context, options) { const authResponse = await context.post("oauth/token", { data: { grant_type: "client_credentials", client_id: options.client_id, client_secret: options.client_secret, scope: "write" } }); const authData = await authResponse.json(); if (!authData["access_token"]) { throw new Error(`Failed to authenticate with client_id: ${options.client_id}`); } return authData["access_token"]; } static async authenticateWithUserPassword(context, options) { const authResponse = await context.post("oauth/token", { data: { client_id: "administration", grant_type: "password", username: options.admin_username, password: options.admin_password, scope: "write" } }); const authData = await authResponse.json(); if (!authData["access_token"]) { throw new Error(`Failed to authenticate with user: ${options.admin_username}`); } return authData["access_token"]; } isAuthenticated() { return !!this.options["access_token"]; } async refreshAccessToken() { this.options["access_token"] = await _AdminApiContext.authenticateWithClientCredentials(this.context, this.options); this.context = await _AdminApiContext.createApiRequestContext(this.options); } async get(url, options) { return this.handleRequest("get", url, options); } async post(url, options) { return this.handleRequest("post", url, options); } async patch(url, options) { return this.handleRequest("patch", url, options); } async delete(url, options) { return this.handleRequest("delete", url, options); } async fetch(url, options) { return this.handleRequest("fetch", url, options); } async head(url, options) { return this.handleRequest("head", url, options); } async handleRequest(method, url, options) { const methodMap = { get: this.context.get.bind(this.context), post: this.context.post.bind(this.context), patch: this.context.patch.bind(this.context), delete: this.context.delete.bind(this.context), fetch: this.context.fetch.bind(this.context), head: this.context.head.bind(this.context) }; let response = await methodMap[method](url, options); if (response.status() === 401) { await this.refreshAccessToken(); const updatedOptions = { ...options, data: options?.data ?? void 0, headers: { ...options?.headers || {}, Authorization: `Bearer ${this.options["access_token"]}` } }; response = await methodMap[method](url, updatedOptions); } return response; } }; __publicField$11(_AdminApiContext, "defaultOptions", { app_url: process.env["ADMIN_API_URL"] || process.env["APP_URL"], client_id: process.env["SHOPWARE_ACCESS_KEY_ID"], client_secret: process.env["SHOPWARE_SECRET_ACCESS_KEY"], admin_username: process.env["SHOPWARE_ADMIN_USERNAME"] || "admin", admin_password: process.env["SHOPWARE_ADMIN_PASSWORD"] || "shopware", ignoreHTTPSErrors: true }); let AdminApiContext = _AdminApiContext; var __defProp$10 = Object.defineProperty; var __defNormalProp$10 = (obj, key, value) => key in obj ? __defProp$10(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField$10 = (obj, key, value) => { __defNormalProp$10(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; const _StoreApiContext = class _StoreApiContext { constructor(context, options) { __publicField$10(this, "context"); __publicField$10(this, "options"); this.context = context; this.options = options; } static async create(options) { const contextOptions = { ...this.defaultOptions, ...options }; return new _StoreApiContext(await this.createContext(contextOptions), contextOptions); } static async createContext(options) { const extraHTTPHeaders = { "Accept": "application/json", "Content-Type": "application/json" }; if (options["sw-access-key"]) { extraHTTPHeaders["sw-access-key"] = options["sw-access-key"]; } if (options["sw-context-token"]) { extraHTTPHeaders["sw-context-token"] = options["sw-context-token"]; } return await request.newContext({ baseURL: `${options["app_url"]}store-api/`, ignoreHTTPSErrors: options.ignoreHTTPSErrors, extraHTTPHeaders }); } async login(user) { const loginResponse = await this.post(`account/login`, { data: { username: user.email, password: user.password } }); const responseHeaders = loginResponse.headers(); if (!responseHeaders["sw-context-token"]) { throw new Error(`Failed to login with user: ${user.email}`); } this.options["sw-context-token"] = responseHeaders["sw-context-token"]; this.context = await _StoreApiContext.createContext(this.options); return responseHeaders; } async get(url, options) { return this.context.get(url, options); } async post(url, options) { return this.context.post(url, options); } async patch(url, options) { return this.context.patch(url, options); } async delete(url, options) { return this.context.delete(url, options); } async fetch(url, options) { return this.context.fetch(url, options); } async head(url, options) { return this.context.head(url, options); } }; __publicField$10(_StoreApiContext, "defaultOptions", { app_url: process.env["APP_URL"], ignoreHTTPSErrors: true }); let StoreApiContext = _StoreApiContext; var __defProp$$ = Object.defineProperty; var __defNormalProp$$ = (obj, key, value) => key in obj ? __defProp$$(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField$$ = (obj, key, value) => { __defNormalProp$$(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; class MailpitApiContext { constructor(context) { __publicField$$(this, "context"); this.context = context; } /** * Fetches email headers based on the recipient's email address. * @param email - The email address of the recipient. * @returns An Email object containing the email headers. */ async getEmailHeaders(email) { const response = await this.context.get("/api/v1/search", { params: { kind: "To.Address", query: email } }); const responseJson = await response.json(); const message = responseJson.messages[0]; return { fromName: message.From.Name, fromAddress: message.From.Address, toName: message.To[0].Name, toAddress: message.To[0].Address, subject: message.Subject, emailId: message.ID }; } /** * Retrieves the body content of the email as an HTML string. * @param email - The email address of the recipient. * @returns A promise that resolves to the HTML content of the email. */ async getEmailBody(email) { const emailId = (await this.getEmailHeaders(email)).emailId; const response = await this.context.get(`view/${emailId}.html`); const buffer = await response.body(); return buffer.toString("utf-8"); } /** * Generates the full email content, combining headers and body. * @param email - The email address to fetch headers for. * @returns A promise that resolves to the full email content as a string. */ async generateEmailContent(email) { const headers = await this.getEmailHeaders(email); const htmlTemplate = await this.getEmailBody(email); const headerSection = ` <div style="font-family:arial; font-size:16px;" id="email-container"> <p id="from"><strong>From:</strong> ${headers.fromName} &lt;${headers.fromAddress}&gt;</p> <p id="to"><strong>To:</strong> ${headers.toName} &lt;${headers.toAddress}&gt;</p> <p id="subject"><strong>Subject:</strong> ${headers.subject}</p> </div> `; return headerSection + htmlTemplate; } /** * Retrieves the plain text content of the email. * @param email - The email address of the recipient. * @returns A promise that resolves to the plain text content of the email. */ async getRenderMessageTxt(email) { const emailId = (await this.getEmailHeaders(email)).emailId; const response = await this.context.get(`view/${emailId}.txt`); const buffer = await response.body(); return buffer.toString("utf-8"); } /** * Extracts the first URL found in the plain text content of the latest email. * @param email - The email address of the recipient. * @returns A promise that resolves to the first URL found in the email content. * @throws An error if no URL is found in the email content. */ async getLinkFromMail(email) { const textContent = await this.getRenderMessageTxt(email); const urlMatch = textContent.match(/https?:\/\/[^\s]+/); if (urlMatch) { return urlMatch[0]; } throw new Error("No URL found in the email content"); } /** * Deletes a specific email by ID if provided, or deletes all emails if no ID is provided. * @param emailId - The ID of the email to delete (optional). */ async deleteMail(emailId) { const data = emailId ? { IDs: [emailId] } : {}; await this.context.delete(`api/v1/messages`, { data }); } /** * Creates a new MailpitApiContext instance with the appropriate configuration. * @param baseURL - The base URL for the API. * @returns A promise that resolves to a MailpitApiContext instance. */ static async create(baseURL) { const extraHTTPHeaders = { "Accept": "application/json", "Content-Type": "application/json" }; const context = await request.newContext({ baseURL, ignoreHTTPSErrors: true, extraHTTPHeaders }); return new MailpitApiContext(context); } } const test$c = test$f.extend({ AdminApiContext: [ async ({}, use) => { const adminApiContext = await AdminApiContext.create(); await use(adminApiContext); }, { scope: "worker" } ], StoreApiContext: [ async ({ DefaultSalesChannel }, use) => { const options = { app_url: process.env["APP_URL"], "sw-access-key": DefaultSalesChannel.salesChannel.accessKey, ignoreHTTPSErrors: true }; const storeApiContext = await StoreApiContext.create(options); await use(storeApiContext); }, { scope: "worker" } ], MailpitApiContext: [ async ({}, use) => { const mailpitApiContext = await MailpitApiContext.create(process.env["MAILPIT_BASE_URL"]); await use(mailpitApiContext); }, { scope: "worker" } ] }); async function mockApiCalls(page) { await page.route("**/api/notification/message*", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ notifications: [], timestamp: "2024-06-19 06:23:25.040" }) })); await page.route("**/api/_action/store/plugin/search*", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ items: [], total: 0 }) })); await page.route("**/api/_action/store/updates*", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ items: [], total: 0 }) })); await page.route("**/api/sbp/shop-info*", (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ items: [], total: 0 }) })); await page.route("**/api/sbp/shop-info*", (route) => route.fulfill({ status: 200, contentType: "application/json", body: process.env.SBP_SHOP_INFO_JSON ?? "{}" })); await page.route("**/api/sbp/bookableplans*", (route) => route.fulfill({ status: 200, contentType: "application/json", body: process.env.SBP_BOOKABLE_PLANS_JSON ?? "{}" })); await page.route("**/api/sbp/nps/active-trigger", (route) => route.fulfill({ status: 200, contentType: "application/json", body: '{"prompt":false,"trigger":["gone-live"]}' })); } const isSaaSInstance = async (adminApiContext) => { const instanceFeatures = await adminApiContext.get("./instance/features"); return instanceFeatures.ok(); }; const isThemeCompiled = async (context, storefrontUrl) => { const response = await context.get(storefrontUrl); const body = (await response.body()).toString(); const matches = body.match(/.*"(https?:\/\/.*all\.css[^"]*)".*/); if (matches && matches?.length > 1) { const allCssUrl = matches[1]; const allCssResponse = await context.get(allCssUrl); return allCssResponse.status() < 400; } return false; }; const clearDelayedCache = async (adminApiContext) => { await adminApiContext.delete("./_action/cache-delayed"); }; const test$b = test$f.extend({ AdminPage: async ({ IdProvider, AdminApiContext, SalesChannelBaseConfig, browser }, use) => { const context = await browser.newContext({ baseURL: SalesChannelBaseConfig.adminUrl, serviceWorkers: "block" }); const page = await context.newPage(); await mockApiCalls(page); const { id, uuid } = IdProvider.getIdPair(); const adminUser = { id: uuid, username: `admin_${id}`, firstName: `${id} admin`, lastName: `${id} admin`, localeId: SalesChannelBaseConfig.enGBLocaleId, email: `admin_${id}@example.com`, timezone: "Europe/Berlin", password: "shopware", admin: true }; const response = await AdminApiContext.post("user", { data: adminUser }); expect(response.ok()).toBeTruthy(); await page.goto("#/login"); await page.addStyleTag({ content: ` .sf-toolbar { width: 0 !important; height: 0 !important; display: none !important; pointer-events: none !important; } `.trim() }); await expect(page.url()).toContain("login"); await expect(page.getByLabel(/Username|Email address/)).toBeVisible({ timeout: 9e4 }); await page.getByLabel(/Username|Email address/).fill(adminUser.username); await page.getByLabel("Password", { exact: true }).fill(adminUser.password); const config = await (await AdminApiContext.get("./_info/config")).json(); const jsLoadingPromises = []; for (const i in config.bundles) { if (config.bundles[i]?.js && config.bundles[i]?.js?.length) { const js = config?.bundles[i]?.js ?? []; jsLoadingPromises.push(...js.map((url) => page.waitForResponse(url))); } } await page.getByRole("button", { name: "Log in" }).click(); await Promise.all(jsLoadingPromises); const originalReload = page.reload.bind(page); page.reload = async () => { const res = await originalReload(); await page.addStyleTag({ content: ` .sf-toolbar { width: 0 !important; height: 0 !important; display: none !important; pointer-events: none !important; } `.trim() }); return res; }; await clearDelayedCache(AdminApiContext); await expect(page.locator(".sw-skeleton")).toHaveCount(0); await page.waitForURL((url) => { return url.hash !== "#login"; }); await expect(page.getByText("Administrator").first()).toBeVisible({ timeout: 6e4 }); await use(page); await page.close(); await context.close(); await AdminApiContext.delete(`user/${uuid}`); }, StorefrontPage: async ({ DefaultSalesChannel, SalesChannelBaseConfig, browser, AdminApiContext, InstanceMeta }, use) => { const { url, salesChannel } = DefaultSalesChannel; const context = await browser.newContext({ baseURL: url }); let page; if (!await isThemeCompiled(AdminApiContext, DefaultSalesChannel.url)) { test$f.slow(); await AdminApiContext.post( `./_action/theme/${SalesChannelBaseConfig.defaultThemeId}/assign/${salesChannel.id}` ); await clearDelayedCache(AdminApiContext); page = await context.newPage(); if (InstanceMeta.isSaaS) { while (!await isThemeCompiled(AdminApiContext, DefaultSalesChannel.url)) { await clearDelayedCache(AdminApiContext); await page.waitForTimeout(4e3); } } } else { page = await context.newPage(); } await page.goto("./", { waitUntil: "load" }); await use(page); await page.close(); await context.close(); }, InstallPage: async ({ browser }, use) => { const context = await browser.newContext({ baseURL: process.env["APP_URL"] }); const page = await context.newPage(); await use(page); await page.close(); await context.close(); }, page: async ({ AdminPage }, use) => { await use(AdminPage); }, context: async ({ AdminPage }, use) => { await use(AdminPage.context()); } }); var __defProp$_ = Object.defineProperty; var __defNormalProp$_ = (obj, key, value) => key in obj ? __defProp$_(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField$_ = (obj, key, value) => { __defNormalProp$_(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; class Actor { constructor(name, page, baseURL) { __publicField$_(this, "page"); __publicField$_(this, "name"); __publicField$_(this, "baseURL"); __publicField$_(this, "expects", expect); this.name = name; this.page = page; this.baseURL = baseURL; } async attemptsTo(task) { const stepTitle = `${this.name} attempts to ${this.camelCaseToLowerCase(task.name)}`; await test$f.step(stepTitle, async () => await task()); } async goesTo(url, forceReload = false) { const stepTitle = `${this.name} navigates to "${url}"`; await test$f.step(stepTitle, async () => { if (this.baseURL && !forceReload && url.startsWith("#")) { const baseURLWithoutSlash = this.baseURL.charAt(this.baseURL.length - 1) == "/" ? this.baseURL.substr(0, this.baseURL.length - 1) : this.baseURL; const fullURL = new URL(url, baseURLWithoutSlash); await this.page.evaluate(`document.location = "${url}";`); await this.page.waitForURL(`${fullURL.toString()}**`, { timeout: 15e3 }); await expect(this.page.locator(".sw-skeleton")).toHaveCount(0); } else { await this.page.goto(url); await this.page.addStyleTag({ content: ` .sf-toolbar { width: 0 !important; height: 0 !important; display: none !important; pointer-events: none !important; } `.trim() }); } }); } camelCaseToLowerCase(str) { return str.replace(/[A-Z]/g, (letter) => ` ${letter.toLowerCase()}`); } } const test$a = test$f.extend({ ShopCustomer: async ({ StorefrontPage }, use) => { const shopCustomer = new Actor("Shop customer", StorefrontPage); await use(shopCustomer); }, ShopAdmin: async ({ AdminPage, SalesChannelBaseConfig }, use) => { const shopAdmin = new Actor("Shop administrator", AdminPage, SalesChannelBaseConfig.adminUrl); await use(shopAdmin); } }); function createRandomImage(width = 800, height = 600) { const buffer = Buffer.alloc(width * height * 4); let i = 0; while (i < buffer.length) { buffer[i++] = Math.floor(Math.random() * 256); } return new Image(width, height, buffer); } const getLanguageData = async (languageCode, adminApiContext) => { const resp = await adminApiContext.post("search/language", { data: { limit: 1, filter: [{ type: "equals", field: "translationCode.code", value: languageCode }], associations: { translationCode: {} } } }); const result = await resp.json(); if (result.data.length === 0) { throw new Error(`Language ${languageCode} not found`); } return result.data[0]; }; const getSnippetSetId = async (languageCode, adminApiContext) => { const resp = await adminApiContext.post("search/snippet-set", { data: { limit: 1, filter: [{ type: "equals", field: "iso", value: languageCode }] } }); const result = await resp.json(); return result.data[0].id; }; const getCurrency = async (isoCode, adminApiContext) => { const resp = await adminApiContext.post("search/currency", { data: { limit: 1, filter: [{ type: "equals", field: "isoCode", value: isoCode }] } }); const result = await resp.json(); if (result.data.length === 0) { throw new Error(`Currency ${isoCode} not found`); } return result.data[0]; }; const getTaxId = async (adminApiContext) => { const resp = await adminApiContext.post("search/tax", { data: { limit: 1 } }); const result = await resp.json(); return result.data[0].id; }; const getPaymentMethodId = async (adminApiContext, handlerId) => { const handler = handlerId || "Shopware\\Core\\Checkout\\Payment\\Cart\\PaymentHandler\\InvoicePayment"; const resp = await adminApiContext.post("search/payment-method", { data: { limit: 1, filter: [{ type: "equals", field: "handlerIdentifier", value: handler }] } }); const result = await resp.json(); return result.data[0].id; }; const getDefaultShippingMethodId = async (adminApiContext) => { const resp = await adminApiContext.post("search/shipping-method", { data: { limit: 1, filter: [{ type: "equals", field: "name", value: "Standard" }] } }); const result = await resp.json(); return result.data[0].id; }; const getShippingMethodId = async (name, adminApiContext) => { const resp = await adminApiContext.post("search/shipping-method", { data: { limit: 1, filter: [{ type: "equals", field: "name", value: name }] } }); const result = await resp.json(); return result.data[0].id; }; const getCountryId = async (iso2, adminApiContext) => { const resp = await adminApiContext.post("search/country", { data: { limit: 1, filter: [{ type: "equals", field: "iso", value: iso2 }] } }); const result = await resp.json(); return result.data[0].id; }; const getThemeId = async (technicalName, adminApiContext) => { const resp = await adminApiContext.post("search/theme", { data: { limit: 1, filter: [{ type: "equals", field: "technicalName", value: technicalName }] } }); const result = await resp.json(); return result.data[0].id; }; const getSalutationId = async (salutationKey, adminApiContext) => { const resp = await adminApiContext.post("search/salutation", { data: { limit: 1, filter: [{ type: "equals", field: "salutationKey", value: salutationKey }] } }); const result = await resp.json(); return result.data[0].id; }; const getStateMachineId = async (technicalName, adminApiContext) => { const resp = await adminApiContext.post("search/state-machine", { data: { limit: 1, filter: [{ type: "equals", field: "technicalName", value: technicalName }] } }); const result = await resp.json(); return result.data[0].id; }; const getStateMachineStateId = async (stateMachineId, adminApiContext) => { const resp = await adminApiContext.post("search/state-machine-state", { data: { limit: 1, filter: [{ type: "equals", field: "stateMachineId", value: stateMachineId }] } }); const result = await resp.json(); return result.data[0].id; }; const getFlowId = async (flowName, adminApiContext) => { const resp = await adminApiContext.post("./search/flow", { data: { limit: 1, filter: [{ type: "equals", field: "name", value: flowName }] } }); const result = await resp.json(); return result.data[0].id; }; const getOrderTransactionId = async (orderId, adminApiContext) => { const orderTransactionResponse = await adminApiContext.get(`order/${orderId}/transactions?_response`); const { data: orderTransaction } = await orderTransactionResponse.json(); return orderTransaction[0].id; }; const getMediaId = async (fileName, adminApiContext) => { const resp = await adminApiContext.post("./search/media", { data: { limit: 1, filter: [{ type: "equals", field: "fileName", value: fileName }] } }); const result = await resp.json(); return result.data[0].id; }; const getFlowTemplate = async (flowTemplateId, adminApiContext) => { const flowTemplateResponse = await adminApiContext.post(`search/flow-template`, { data: { limit: 1, filter: [{ type: "equals", field: "id", value: flowTemplateId }] } }); const result = await flowTemplateResponse.json(); return result.data[0]; }; const getFlow = async (flowId, adminApiContext) => { const flowResponse = await adminApiContext.post(`search/flow`, { data: { limit: 1, filter: [{ type: "equals", field: "id", value: flowId }], associations: { sequences: {} } } }); const result = await flowResponse.json(); return result.data[0]; }; const compareFlowTemplateWithFlow = async (flowId, flowTemplateId, adminApiContext) => { const flowTemplateData = await getFlowTemplate(flowTemplateId, adminApiContext); const flowData = await getFlow(flowId, adminApiContext); if (flowTemplateData.config.eventName != flowData.eventName) { return false; } let i = 0; for (const sequenceTemplate of flowTemplateData.config.sequences) { if (sequenceTemplate.actionName != flowData.sequences[i].actionName) { return false; } if (JSON.stringify(sequenceTemplate.config) != JSON.stringify(flowData.sequences[i].config)) { return false; } i++; } return true; }; function extractIdFromUrl(url) { const segments = url.split("/"); return segments.length > 0 ? segments[segments.length - 1] : null; } const setOrderStatus = async (orderId, orderStatus, adminApiContext) => { return await adminApiContext.post(`./_action/order/${orderId}/state/${orderStatus}`); }; const getPromotionWithDiscount = async (promotionId, adminApiContext) => { const resp = await adminApiContext.post("search/promotion", { data: { limit: 1, associations: { discounts: { limit: 10, type: "equals", field: "promotionId", value: promotionId } }, filter: [{ type: "equals", field: "id", value: promotionId }] } }); const { data: promotion } = await resp.json(); return promotion[0]; }; var __defProp$Z = Object.defineProperty; var __defNormalProp$Z = (obj, key, value) => key in obj ? __defProp$Z(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField$Z = (obj, key, value) => { __defNormalProp$Z(obj, typeof key !== "symbol" ? key + "" : key, value); return value; }; class TestDataService { constructor(AdminApiClient, IdProvider, options) { __publicField$Z(this, "AdminApiClient"); __publicField$Z(this, "IdProvider"); __publicField$Z(this, "namePrefix", "Test-"); __publicField$Z(this, "nameSuffix", ""); __publicField$Z(this, "defaultSalesChannel"); __publicField$Z(this, "defaultTaxId"); __publicField$Z(this, "defaultCurrencyId"); __publicField$Z(this, "defaultCategoryId"); __publicField$Z(this, "defaultLanguageId"); __publicField$Z(this, "defaultCountryId"); __publicField$Z(this, "defaultCustomerGroupId"); /** * Configures if an automated cleanup of the data should be executed. * * @private */ __publicField$Z(this, "shouldCleanUp", true); /** * Configuration of higher priority entities for the cleanup operation. * These entities will be deleted before others. * This will prevent restricted delete operations of associated entities. * * @private */ __publicField$Z(this, "highPriorityEntities", ["order", "product", "product_cross_selling", "landing_page", "shipping_method", "sales_channel_domain", "sales_channel_currency", "sales_channel_country", "sales_channel_payment_method", "customer"]); /** * A registry of all created records. * * @private */ __publicField$Z(this, "createdRecords", []); __publicField$Z(this, "restoreSystemConfig", {}); /** * A registry of all created sales channel records. * * @private */ __publicField$Z(this, "createdSalesChannelRecords", []); /** * Function that generates combinations from n number of arrays * with m number of elements in them. * @param array */ __publicField$Z(this, "combineAll", (array) => { const result = []; const max = array.length - 1; const helper = (tmpArray, i) => { for (let j = 0, l = array[i].length; j < l; j++) { const copy = tmpArray.slice(0); copy.push(array[i][j]); if (i == max) result.push(copy); else helper(copy, i + 1); } }; helper([], 0); return result; }); this.AdminApiClient = AdminApiClient; this.IdProvider = IdProvider; this.defaultSalesChannel = options.defaultSalesChannel; this.defaultTaxId = options.defaultTaxId; this.defaultCurrencyId = options.defaultCurrencyId; this.defaultCategoryId = options.defaultCategoryId; this.defaultLanguageId = options.defaultLanguageId; this.defaultCountryId = options.defaultCountryId; this.defaultCustomerGroupId = options.defaultCustomerGroupId; if (options.namePrefix) { this.namePrefix = options.namePrefix; } if (options.nameSuffix) { this.nameSuffix = options.nameSuffix; } } /** * Creates a basic product without images or other special configuration. * The product will be added to the default sales channel category if configured. * * @param overrides - Specific data overrides that will be applied to the product data struct. * @param taxId - The uuid of the tax rule to use for the product pricing. * @param currencyId - The uuid of the currency to use for the product pricing. */ async createBasicProduct(overrides = {}, taxId = this.defaultTaxId, currencyId = this.defaultCurrencyId) { if (!taxId) { return Promise.reject("Missing tax ID for creating product."); } if (!currencyId) { return Promise.reject("Missing currency ID for creating product."); } const basicProduct = this.getBasicProductStruct(taxId, currencyId, overrides); const productResponse = await this.AdminApiClient.post("./product?_response=detail", { data: basicProduct }); expect(productResponse.ok()).toBeTruthy(); const { data: product } = await productResponse.json(); this.addCreatedRecord("product", product.id); return product; } /** * Creates a basic product cross-selling entity without products. * * @param productId - The uuid of the product to which the pproduct cross-selling should be assigned. * @param overrides - Specific data overrides that will be applied to the property group data struct. */ async createProductCrossSelling(productId, overrides = {}) { const crossSellingStruct = this.getBasicCrossSellingStruct(productId, overrides); const response = await this.AdminApiClient.post("product-cross-selling?_response=detail", { data: crossSellingStruct }); expect(response.ok()).toBeTruthy(); const { data: productCrossSelling } = await response.json(); this.addCreatedRecord("product_cross_selling", productCrossSelling.id); return productCrossSelling; } /** * Creates a basic product with one randomly generated image. * The product will be added to the default sales channel category if configured. * * @param overrides - Specific data overrides that will be applied to the product data struct. * @param taxId - The uuid of the tax rule to use for the product pricing. * @param currencyId - The uuid of the currency to use for the product pricing. */ async createProductWithImage(overrides = {}, taxId = this.defaultTaxId, currencyId = this.defaultCurrencyId) { const product = await this.createBasicProduct(overrides, taxId, currencyId); const media = await this.createMediaPNG(); await this.assignProductMedia(product.id, media.id); return product; } /** * Creates a digital product with a text file as its download. * The product will be added to the default sales channel category if configured. * * @param content - The content of the text file for the product download. * @param overrides - Specific data overrides that will be applied to the product data struct. * @param taxId - The uuid of the tax rule to use for the product pricing. * @param currencyId - The uuid of the currency to use for the product pricing. */ async createDigitalProduct(content = "Lorem ipsum dolor", overrides = {}, taxId = this.defaultTaxId, currencyId = this.defaultCurrencyId) { const product = await this.createBasicProduct(overrides, taxId, currencyId); const media = await this.createMediaTXT(content); await this.assignProductDownload(product.id, media.id); return product; } /** * Creates a basic product with a price range matrix. * The product will be added to the default sales channel category if configured. * * @param overrides - Specific data overrides that will be applied to the product data struct. * @param taxId - The uuid of the tax rule to use for the product pricing. * @param currencyId - The uuid of the currency to use for the product pricing. */ async createProductWithPriceRange(overrides = {}, taxId = this.defaultTaxId, currencyId = this.defaultCurrencyId) { if (!currencyId) { return Promise.reject("Missing currency ID for creating product."); } const rule = await this.getRule("Always valid (Default)"); const priceRange = this.getProductPriceRangeStruct(currencyId, rule.id); const productOverrides = Object.assign({}, priceRange, overrides); return this.createBasicProduct(productOverrides, taxId, currencyId); } /** * Creates basic variant products based on property group. * * @param parentProduct Parent product of the variants * @param propertyGroups Property group collection which contain options * @param overrides - Specific data overrides that will be applied to the variant data struct. */ async createVariantProducts(parentProduct, propertyGroups, overrides = {}) { const productVariantCandidates = []; for (const propertyGroup of propertyGroups) { const propertyGroupOptions = await this.getPropertyGroupOptions(propertyGroup.id); const propertyGroupOptionsCollection = []; for (const propertyGroupOption of propertyGroupOptions) { propertyGroupOptionsCollection.push({ id: propertyGroupOption.id }); const productConfiguratorResponse = await this.AdminApiClient.post("product-configurator-setting?_response=detail", { data: { id: this.IdProvider.getIdPair().uuid, productId: parentProduct.id, optionId: propertyGroupOption.id } }); expect(productConfiguratorResponse.ok()).toBeTruthy(); } productVariantCandidates.push(propertyGroupOptionsCollection); } const productVariantCombinations = this.combineAll(productVariantCandidates); const variantProducts = []; let index = 1; for (const productVariantCombination of productVariantCombinations) { const variantOverrides = { parentId: parentProduct.id, productNumber: parentProduct.productNumber + "." + index, options: productVariantCombination }; const overrideCollection = Object.assign({}, overrides, variantOverrides); variantProducts.push(await this.createBasicProduct(overrideCollection)); index++; } await this.AdminApiClient.post("_action/indexing/product.indexer?_response=detail", { data: { offset: 0 } }); return variantProducts; } /** * Creates a product review * * @param productId - The uuid of the product to which the review should be assigned. * @param overrides - Specific data overrides that will be applied to the review data struct. */ async createProductReview(productId, overrides = {}) { const basicProductReview = this.getBasicProductReviewStruct(productId, overrides); const productReviewResponse = await this.AdminApiClient.post("product-review?_response=detail", { data: basicProductReview }); expect(productReviewResponse.ok()).toBeTruthy(); const { data: review } = await productReviewResponse.json(); return review; } /** * Creates a basic manufacturer without images or other special configuration. * * @param overrides - Specific data overrides that will be applied to the manufacturer data struct. */ async createBasicManufacturer(overrides = {}) { const basicManufacturer = this.getBasicManufacturerStruct(overrides); const manufacturerResponse = await this.AdminApiClient.post("./product-manufacturer?_response=detail", { data: basicManufacturer }); expect(manufacturerResponse.ok()).toBeTruthy(); const { data: manufacturer } = await manufacturerResponse.json(); this.addCreatedRecord("product_manufacturer", manufacturer.id); return manufacturer; } /** * Creates a basic manufacturer with one randomly generated image. * * @param overrides - Specific data overrides that will be applied to the manufacturer data struct. */ async createManufacturerWithImage(overrides = {}) { const manufacturer = await this.createBasicManufacturer(overrides); const media = await this.createMediaPNG(); await this.assignManufacturerMedia(manufacturer.id, media.id); return manufacturer; } /** * Creates a basic product category to assign products to. * * @param parentId - The uuid of the parent category. * @param overrides - Specific data overrides that will be applied to the category data struct. */ async createCategory(overrides = {}, parentId = this.defaultCategoryId) { const basicCategory = this.getBasicCategoryStruct(overrides, parentId); const response = await this.AdminApiClient.post("category?_response=detail", { data: basicCategory }); expect(response.ok()).toBeTruthy(); const { data: category } = await response.json(); this.addCreatedRecord("category", category.id); return category; } /** * Creates a new media resource containing a random generated PNG image. * * @param width - The width of the image in pixel. Default is 800. * @param height - The height of the image in pixel. Default is 600. */ async createMediaPNG(width = 800, height = 600) { const image = createRandomImage(width, height); const media = await this.createMediaResource(); const filename = `${this.namePrefix}Media-${media.id}${this.nameSuffix}`; const response = await this.AdminApiClient.post(`_action/media/${media.id}/upload?extension=png&fileName=${filename}`, { data: Buffer.from(image.toBuffer()), headers: { "content-type": "image/png" } }); expect(response.ok()).toBeTruthy(); this.addCreatedRecord("media", media.id); return media; } /** * Creates a new media resource containing a text file. * * @param content - The content of the text file. */ async createMediaTXT(content = "Lorem ipsum dolor") { const media = await this.createMediaResource(); const filename = `${this.namePrefix}Media-${media.id}${this.nameSuffix}`; const response = await this.AdminApiClient.post(`_action/media/${media.id}/upload?extension=txt&fileName=${filename}`, { data: content, headers: { "content-type": "application/octet-stream" } }); expect(response.ok()).toBeTruthy(); this.addCreatedRecord("media", media.id); return media; } /** * Creates a new empty media resource. * This method is mostly used to combine it with a certain file upload. */ async createMediaResource() { const id = this.IdProvider.getIdPair().id; const mediaResponse = await this.AdminApiClient.post("media?_response=detail", { data: { private: false, alt: `Alt-${id}`, title: `Title-${id}` } }); expect(mediaResponse.ok()).toBeTruthy(); const { data: media } = await mediaResponse.json(); return media; } /** * Creates a new property group with color type options. * * @param overrides - Specific data overrides that will be applied t