@shopware-ag/acceptance-test-suite
Version:
Shopware Acceptance Test Suite
1,364 lines (1,350 loc) • 352 kB
JavaScript
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} <${headers.fromAddress}></p>
<p id="to"><strong>To:</strong> ${headers.toName} <${headers.toAddress}></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