Skip to content
26 changes: 26 additions & 0 deletions src/page-objects/storefront/CheckoutConfirm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export class CheckoutConfirm implements PageObject {
*/
public readonly cartLineItemImages: Locator;
Comment thread
sguder marked this conversation as resolved.
public readonly page: Page;
public readonly confirmProductTable: Locator;

constructor(page: Page) {
this.page = page;
Expand All @@ -54,6 +55,31 @@ export class CheckoutConfirm implements PageObject {
this.shippingExpress = page.getByLabel(translate("storefront:checkout:common.express"));

this.cartLineItemImages = page.locator(".line-item-img-link");
Comment thread
sguder marked this conversation as resolved.
this.confirmProductTable = page.locator(".confirm-product");
}

getLineItemByProductName(productName: string): Record<string, Locator> {
const productLineItem = this.confirmProductTable.locator(".line-item-product", { hasText: productName });
const productNameLabel = productLineItem.locator(".line-item-label");
const productTotalPrice = productLineItem.locator(".line-item-total-price-value");

return {
productLineItem: productLineItem,
productNameLabel: productNameLabel,
productTotalPrice: productTotalPrice,
};
}

getLineItemByPromotionName(promotionName: string): Record<string, Locator> {
const promotionLineItem = this.confirmProductTable.locator(".line-item-promotion", { hasText: promotionName });
const promotionNameLabel = promotionLineItem.locator(".line-item-label");
const promotionTotalPrice = promotionLineItem.locator(".line-item-total-price-value");

return {
promotionLineItem: promotionLineItem,
promotionNameLabel: promotionNameLabel,
promotionTotalPrice: promotionTotalPrice,
};
}

url() {
Expand Down
11 changes: 11 additions & 0 deletions src/page-objects/storefront/OffCanvasCart.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,15 @@ export class OffCanvasCart implements PageObject {
wishlistNotAddedButton: wishlistNotAddedButton,
};
}

async getLineItemByPromotionName(name: string): Promise<Record<string, Locator>> {
const promotionItem = this.page.locator(".line-item-promotion", { hasText: name });
const promotionLabel = promotionItem.locator(".line-item-label");
const promotionPrice = promotionItem.locator(".line-item-total-price-value");

return {
promotionLabel: promotionLabel,
promotionPrice: promotionPrice,
};
}
}
164 changes: 151 additions & 13 deletions src/services/TestDataService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type {
Language,
Manufacturer,
Media,
NewsletterRecipient,
Order,
OrderDelivery,
OrderLineItem,
Expand Down Expand Up @@ -59,6 +60,23 @@ export interface SimpleLineItem {
overrides?: Partial<OrderLineItem>;
}

export interface PromotionWithConditionRuleOptions {
id: string;
name: string;
ruleId: string;
useCode?: boolean;
discountValue?: number;
discountScope?: string;
discountType?: string;
salesChannelId?: string;
}

export interface BasicRuleCondition {
type: string;
value: Record<string, unknown>;
children?: BasicRuleCondition[];
}

export interface SyncApiOperation {
entity: string;
action: "upsert" | "delete";
Expand Down Expand Up @@ -164,6 +182,44 @@ export class TestDataService {
}
}

/**
* Creates a newsletter recipient for a customer in the default sales channel.
* If a matching recipient already exists, it will be reused and registered for cleanup.
*
* @param customer - The customer whose email and profile data will be used for the newsletter recipient.
* @param overrides - Specific data overrides that will be applied to the newsletter recipient payload.
*/
async createNewsletterRecipient(customer: Customer, overrides: Partial<NewsletterRecipient> = {}): Promise<NewsletterRecipient> {
const hash = this.IdProvider.getIdPair();
const recipientPayload = {
email: customer.email,
salesChannelId: this.defaultSalesChannel.id,
firstName: customer.firstName ?? "Test",
lastName: customer.lastName ?? "User",
hash: customer.id || hash,
status: "direct",
languageId: customer.languageId,
salutationId: customer.salutationId,
confirmedAt: new Date().toISOString(),
...overrides,
};

const existingNewsletterRecipient = await this.getNewsletterRecipient({ ...customer, email: recipientPayload.email });
if (existingNewsletterRecipient) {
this.addCreatedRecord("newsletter_recipient", existingNewsletterRecipient.id);
return existingNewsletterRecipient;
}

const resp = await this.AdminApiClient.post("newsletter-recipient?_response=detail", {
data: recipientPayload,
});

expect(resp.ok()).toBeTruthy();
const { data: recipient } = (await resp.json()) as { data: NewsletterRecipient };
this.addCreatedRecord("newsletter_recipient", recipient.id);
return recipient;
}

/**
* Creates a basic product without images or other special configuration.
* The product will be added to the default sales channel category if configured.
Expand Down Expand Up @@ -754,6 +810,55 @@ export class TestDataService {
return promotionWithDiscount;
}

/**
* Creates a promotion with one discount and assigns a condition rule as a persona rule.
* The created promotion and assigned rule are both registered for cleanup.
*
* @param promotionConfig - Promotion configuration, including the promotion id, name, and assigned rule id.
*/
async createPromotionWithConditionRule(promotionConfig: PromotionWithConditionRuleOptions): Promise<Promotion> {
const useCode = promotionConfig.useCode ?? false;
const basicPromotion = this.getBasicPromotionStruct(promotionConfig.salesChannelId, {
id: promotionConfig.id,
name: promotionConfig.name,
useCodes: useCode,
useIndividualCodes: useCode,
discounts: [
{
scope: promotionConfig.discountScope ?? "cart",
type: promotionConfig.discountType ?? "percentage",
value: promotionConfig.discountValue ?? 10,
considerAdvancedRules: false,
},
],
});

if (!useCode) {
delete basicPromotion.code;
}

const promotionWithConditionRule = {
...basicPromotion,
personaRules: [
{
id: promotionConfig.ruleId,
},
],
};

const promotionResponse = await this.AdminApiClient.post("promotion?_response=detail", {
data: promotionWithConditionRule,
});
expect(promotionResponse.ok()).toBeTruthy();

const { data: promotion } = (await promotionResponse.json()) as { data: Promotion };

this.addCreatedRecord("promotion", promotionConfig.id);
this.addCreatedRecord("rule", promotionConfig.ruleId);

return promotion;
}

/**
* Creates a new basic payment method.
*
Expand Down Expand Up @@ -826,12 +931,14 @@ export class TestDataService {
}

/**
* Creates a new basic rule with the condition cart amount >= 1.
* Creates a new basic rule with the condition cart amount >= 1 by default.
* Pass a condition object to create the same basic rule structure with another condition.
*
* @param overrides - Specific data overrides that will be applied to the basic rule data struct.
* @param condition - The condition object or condition type used in the generated rule.
*/
async createBasicRule(overrides: Partial<Rule> = {}, conditionType = "cartCartAmount", operator = ">=", amount = 1): Promise<Rule> {
const basicRule = this.getBasicRuleStruct(overrides, conditionType, operator, amount);
async createBasicRule(overrides: Partial<Rule> = {}, condition: BasicRuleCondition | string = "cartCartAmount", operator = ">=", amount = 1): Promise<Rule> {
const basicRule = this.getBasicRuleStruct(overrides, condition, operator, amount);

const ruleResponse = await this.AdminApiClient.post("rule?_response=detail", {
data: basicRule,
Expand Down Expand Up @@ -1490,6 +1597,35 @@ export class TestDataService {
return aclUser;
}

/**
* Retrieves a newsletter recipient by customer email and the default sales channel.
* @param customer - The customer whose email will be used to search for a newsletter recipient.
*/
async getNewsletterRecipient(customer: Customer): Promise<NewsletterRecipient | undefined> {
const response = await this.AdminApiClient.post("search/newsletter-recipient", {
data: {
limit: 1,
filter: [
{
type: "equals",
field: "email",
value: customer.email,
},
{
type: "equals",
field: "salesChannelId",
value: this.defaultSalesChannel.id,
},
],
},
});
expect(response.ok()).toBeTruthy();

const { data: result } = (await response.json()) as { data: NewsletterRecipient[] };

return result[0];
}

/**
* Retrieves a language based on its code.
* @param languageCode
Expand Down Expand Up @@ -2208,9 +2344,19 @@ export class TestDataService {
return Object.assign({}, basicProduct, overrides);
}

getBasicRuleStruct(overrides: Partial<Rule> = {}, conditionType: string, operator: string, amount: number): Partial<Rule> {
getBasicRuleStruct(overrides: Partial<Rule> = {}, condition: BasicRuleCondition | string = "cartCartAmount", operator = ">=", amount = 1): Partial<Rule> {
const { id: ruleId, uuid: ruleUuid } = this.IdProvider.getIdPair();
const ruleName = `${this.namePrefix}Rule-${ruleId}${this.nameSuffix}`;
const ruleCondition =
typeof condition === "string"
? {
type: condition,
value: {
operator: operator,
amount: amount,
},
}
: condition;

const description = `
Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua.
Expand All @@ -2229,15 +2375,7 @@ export class TestDataService {
children: [
{
type: "andContainer",
children: [
{
type: conditionType,
value: {
operator: operator,
amount: amount,
},
},
],
children: [ruleCondition],
},
],
},
Expand Down
8 changes: 8 additions & 0 deletions src/types/ShopwareTypes.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import type { components } from "@shopware/api-client/admin-api-types";

export type NewsletterRecipient = components["schemas"]["NewsletterRecipient"] & {
id: string;
email: string;
salesChannelId: string;
firstName: string;
lastName: string;
};

export type SalesChannel = components["schemas"]["SalesChannel"] & {
id: string;
};
Expand Down
Loading