@shopware-ag/acceptance-test-suite 2.3.10 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,6 +15,7 @@ This test suite is an extension to [Playwright](https://playwright.dev/) to easi
15
15
  * [Actor Pattern](#actor-pattern)
16
16
  * [Data Fixtures](#data-fixtures)
17
17
  * [Code Contribution](#code-contribution)
18
+ * [Best practices](#best-practices)
18
19
 
19
20
  ## Installation
20
21
  Start by creating your own [Playwright](https://playwright.dev/docs/intro) project.
@@ -374,4 +375,32 @@ If you create your own data fixtures make sure to import and merge them in your
374
375
  ## Code Contribution
375
376
  You can contribute to this project via its [official repository](https://github.com/shopware/acceptance-test-suite/) on GitHub.
376
377
 
377
- This project uses [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). Please make sure to form your commits accordingly to the spec.
378
+ This project uses [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/). Please make sure to form your commits accordingly to the spec.
379
+
380
+ ## Best practices
381
+
382
+ A good first read about this is the official [playwright best practices page](https://playwright.dev/docs/best-practices). It describes the most important practices which should also be followed when writing acceptance tests for Shopware.
383
+
384
+ The most important part is [test isolation](https://playwright.dev/docs/best-practices#make-tests-as-isolated-as-possible) which helps to prevent flaky behavior and enables the test to be run in parallel and on systems with an unknown state.
385
+
386
+
387
+ ### Dos
388
+
389
+ - use fixtures or the [`TestDataService`](./src/services/TestDataService.ts)
390
+ - create all the data that is required for your test case. That includes sales channels, customers and users (the page fixtures handle most of the common use cases)
391
+ - if you need specific settings for your test, set it explicitly for the user/customer/sales channel
392
+ - directly jump to detail pages with the id of the entities you've created
393
+ - if that's no possible, use the search with a unique name to filter lists to just that single entity
394
+
395
+ ### Don'ts
396
+
397
+ - do not expect lists/tables to only contain one item, leverage unique ids/names to open or find your entity instead
398
+ - same with helper functions, do not except to only get item back from the API. Always a unique criteria to the API call
399
+ - avoid unused fixtures: if you request a fixture but don't use any data from the fixture, the test or fixture should be refactored
400
+ - do not depend on implicit configuration and existing data. Examples:
401
+ - rules
402
+ - flows
403
+ - categories
404
+ - do not expect the shop to have the defaults en_GB and EUR
405
+ - do not change global settings (sales channel is ok, because it's created by us)
406
+ - basically everything in Settings that is not specific to a sales channel (tax, search, etc.)
package/dist/index.d.mts CHANGED
@@ -70,9 +70,61 @@ declare class StoreApiContext {
70
70
  head<PAYLOAD>(url: string, options?: RequestOptions<PAYLOAD>): Promise<APIResponse>;
71
71
  }
72
72
 
73
+ interface Email {
74
+ fromName: string;
75
+ fromAddress: string;
76
+ toName: string;
77
+ toAddress: string;
78
+ subject: string;
79
+ emailId: string;
80
+ }
81
+ declare class MailpitApiContext {
82
+ context: APIRequestContext;
83
+ constructor(context: APIRequestContext);
84
+ /**
85
+ * Fetches email headers based on the recipient's email address.
86
+ * @param email - The email address of the recipient.
87
+ * @returns An Email object containing the email headers.
88
+ */
89
+ getEmailHeaders(email: string): Promise<Email>;
90
+ /**
91
+ * Retrieves the body content of the latest email as an HTML string.
92
+ * @returns The HTML content of the latest email.
93
+ */
94
+ getEmailBody(): Promise<string>;
95
+ /**
96
+ * Generates the full email content, combining headers and body.
97
+ * @param email - The email address to fetch headers for.
98
+ * @returns The full email content as a string.
99
+ */
100
+ generateEmailContent(email: string): Promise<string>;
101
+ /**
102
+ * Retrieves the plain text content of the latest email.
103
+ * @returns The plain text content of the latest email.
104
+ */
105
+ getRenderMessageTxt(): Promise<string>;
106
+ /**
107
+ * Extracts the first URL found in the plain text content of the latest email.
108
+ * @returns The first URL found in the email content.
109
+ * @throws An error if no URL is found in the email content.
110
+ */
111
+ getLinkFromMail(): Promise<string>;
112
+ /**
113
+ * Deletes a specific email by ID if provided, or deletes all emails if no ID is provided.
114
+ * @param emailId - The ID of the email to delete (optional).
115
+ */
116
+ deleteMail(emailId?: string): Promise<void>;
117
+ /**
118
+ * Creates a new EmailApiContext instance with the appropriate configuration.
119
+ * @returns A promise that resolves to an EmailApiContext instance.
120
+ */
121
+ static create(baseURL: string): Promise<MailpitApiContext>;
122
+ }
123
+
73
124
  interface ApiContextTypes {
74
125
  AdminApiContext: AdminApiContext;
75
126
  StoreApiContext: StoreApiContext;
127
+ MailpitApiContext: MailpitApiContext;
76
128
  }
77
129
 
78
130
  interface PageContextTypes {
@@ -460,7 +512,7 @@ declare class TestDataService {
460
512
  /**
461
513
  * Will delete all entities created by the data service via sync API.
462
514
  */
463
- cleanUp(): Promise<playwright_core.APIResponse>;
515
+ cleanUp(): Promise<playwright_core.APIResponse | null>;
464
516
  /**
465
517
  * Convert a JS date object into a date-time compatible string.
466
518
  *
package/dist/index.d.ts CHANGED
@@ -70,9 +70,61 @@ declare class StoreApiContext {
70
70
  head<PAYLOAD>(url: string, options?: RequestOptions<PAYLOAD>): Promise<APIResponse>;
71
71
  }
72
72
 
73
+ interface Email {
74
+ fromName: string;
75
+ fromAddress: string;
76
+ toName: string;
77
+ toAddress: string;
78
+ subject: string;
79
+ emailId: string;
80
+ }
81
+ declare class MailpitApiContext {
82
+ context: APIRequestContext;
83
+ constructor(context: APIRequestContext);
84
+ /**
85
+ * Fetches email headers based on the recipient's email address.
86
+ * @param email - The email address of the recipient.
87
+ * @returns An Email object containing the email headers.
88
+ */
89
+ getEmailHeaders(email: string): Promise<Email>;
90
+ /**
91
+ * Retrieves the body content of the latest email as an HTML string.
92
+ * @returns The HTML content of the latest email.
93
+ */
94
+ getEmailBody(): Promise<string>;
95
+ /**
96
+ * Generates the full email content, combining headers and body.
97
+ * @param email - The email address to fetch headers for.
98
+ * @returns The full email content as a string.
99
+ */
100
+ generateEmailContent(email: string): Promise<string>;
101
+ /**
102
+ * Retrieves the plain text content of the latest email.
103
+ * @returns The plain text content of the latest email.
104
+ */
105
+ getRenderMessageTxt(): Promise<string>;
106
+ /**
107
+ * Extracts the first URL found in the plain text content of the latest email.
108
+ * @returns The first URL found in the email content.
109
+ * @throws An error if no URL is found in the email content.
110
+ */
111
+ getLinkFromMail(): Promise<string>;
112
+ /**
113
+ * Deletes a specific email by ID if provided, or deletes all emails if no ID is provided.
114
+ * @param emailId - The ID of the email to delete (optional).
115
+ */
116
+ deleteMail(emailId?: string): Promise<void>;
117
+ /**
118
+ * Creates a new EmailApiContext instance with the appropriate configuration.
119
+ * @returns A promise that resolves to an EmailApiContext instance.
120
+ */
121
+ static create(baseURL: string): Promise<MailpitApiContext>;
122
+ }
123
+
73
124
  interface ApiContextTypes {
74
125
  AdminApiContext: AdminApiContext;
75
126
  StoreApiContext: StoreApiContext;
127
+ MailpitApiContext: MailpitApiContext;
76
128
  }
77
129
 
78
130
  interface PageContextTypes {
@@ -460,7 +512,7 @@ declare class TestDataService {
460
512
  /**
461
513
  * Will delete all entities created by the data service via sync API.
462
514
  */
463
- cleanUp(): Promise<playwright_core.APIResponse>;
515
+ cleanUp(): Promise<playwright_core.APIResponse | null>;
464
516
  /**
465
517
  * Convert a JS date object into a date-time compatible string.
466
518
  *
package/dist/index.mjs CHANGED
@@ -393,16 +393,16 @@ const test$b = test$d.extend({
393
393
  ]
394
394
  });
395
395
 
396
- var __defProp$s = Object.defineProperty;
397
- var __defNormalProp$s = (obj, key, value) => key in obj ? __defProp$s(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
398
- var __publicField$s = (obj, key, value) => {
399
- __defNormalProp$s(obj, typeof key !== "symbol" ? key + "" : key, value);
396
+ var __defProp$t = Object.defineProperty;
397
+ var __defNormalProp$t = (obj, key, value) => key in obj ? __defProp$t(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
398
+ var __publicField$t = (obj, key, value) => {
399
+ __defNormalProp$t(obj, typeof key !== "symbol" ? key + "" : key, value);
400
400
  return value;
401
401
  };
402
402
  const _AdminApiContext = class _AdminApiContext {
403
403
  constructor(context, options) {
404
- __publicField$s(this, "context");
405
- __publicField$s(this, "options");
404
+ __publicField$t(this, "context");
405
+ __publicField$t(this, "options");
406
406
  this.context = context;
407
407
  this.options = options;
408
408
  }
@@ -494,7 +494,7 @@ const _AdminApiContext = class _AdminApiContext {
494
494
  return this.context.head(url, options);
495
495
  }
496
496
  };
497
- __publicField$s(_AdminApiContext, "defaultOptions", {
497
+ __publicField$t(_AdminApiContext, "defaultOptions", {
498
498
  app_url: process.env["APP_URL"],
499
499
  client_id: process.env["SHOPWARE_ACCESS_KEY_ID"],
500
500
  client_secret: process.env["SHOPWARE_SECRET_ACCESS_KEY"],
@@ -504,16 +504,16 @@ __publicField$s(_AdminApiContext, "defaultOptions", {
504
504
  });
505
505
  let AdminApiContext = _AdminApiContext;
506
506
 
507
- var __defProp$r = Object.defineProperty;
508
- var __defNormalProp$r = (obj, key, value) => key in obj ? __defProp$r(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
509
- var __publicField$r = (obj, key, value) => {
510
- __defNormalProp$r(obj, typeof key !== "symbol" ? key + "" : key, value);
507
+ var __defProp$s = Object.defineProperty;
508
+ var __defNormalProp$s = (obj, key, value) => key in obj ? __defProp$s(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
509
+ var __publicField$s = (obj, key, value) => {
510
+ __defNormalProp$s(obj, typeof key !== "symbol" ? key + "" : key, value);
511
511
  return value;
512
512
  };
513
513
  const _StoreApiContext = class _StoreApiContext {
514
514
  constructor(context, options) {
515
- __publicField$r(this, "context");
516
- __publicField$r(this, "options");
515
+ __publicField$s(this, "context");
516
+ __publicField$s(this, "options");
517
517
  this.context = context;
518
518
  this.options = options;
519
519
  }
@@ -572,12 +572,122 @@ const _StoreApiContext = class _StoreApiContext {
572
572
  return this.context.head(url, options);
573
573
  }
574
574
  };
575
- __publicField$r(_StoreApiContext, "defaultOptions", {
575
+ __publicField$s(_StoreApiContext, "defaultOptions", {
576
576
  app_url: process.env["APP_URL"],
577
577
  ignoreHTTPSErrors: true
578
578
  });
579
579
  let StoreApiContext = _StoreApiContext;
580
580
 
581
+ var __defProp$r = Object.defineProperty;
582
+ var __defNormalProp$r = (obj, key, value) => key in obj ? __defProp$r(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
583
+ var __publicField$r = (obj, key, value) => {
584
+ __defNormalProp$r(obj, typeof key !== "symbol" ? key + "" : key, value);
585
+ return value;
586
+ };
587
+ class MailpitApiContext {
588
+ constructor(context) {
589
+ __publicField$r(this, "context");
590
+ this.context = context;
591
+ }
592
+ /**
593
+ * Fetches email headers based on the recipient's email address.
594
+ * @param email - The email address of the recipient.
595
+ * @returns An Email object containing the email headers.
596
+ */
597
+ async getEmailHeaders(email) {
598
+ const response = await this.context.get("/api/v1/search", {
599
+ params: {
600
+ kind: "To.Address",
601
+ query: email
602
+ }
603
+ });
604
+ const responseJson = await response.json();
605
+ return {
606
+ fromName: responseJson.messages[0].From.Name,
607
+ fromAddress: responseJson.messages[0].From.Address,
608
+ toName: responseJson.messages[0].To[0].Name,
609
+ toAddress: responseJson.messages[0].To[0].Address,
610
+ subject: responseJson.messages[0].Subject,
611
+ emailId: responseJson.messages[0].ID
612
+ };
613
+ }
614
+ /**
615
+ * Retrieves the body content of the latest email as an HTML string.
616
+ * @returns The HTML content of the latest email.
617
+ */
618
+ async getEmailBody() {
619
+ const response = await this.context.get("view/latest.html");
620
+ const buffer = await response.body();
621
+ const htmlString = buffer.toString("utf-8");
622
+ return htmlString;
623
+ }
624
+ /**
625
+ * Generates the full email content, combining headers and body.
626
+ * @param email - The email address to fetch headers for.
627
+ * @returns The full email content as a string.
628
+ */
629
+ async generateEmailContent(email) {
630
+ const headers = await this.getEmailHeaders(email);
631
+ const htmlTemplate = await this.getEmailBody();
632
+ const headerSection = `
633
+ <div style="font-family:arial; font-size:16px;" id="email-container">
634
+ <p id="from"><strong>From:</strong> ${headers.fromName} &lt;${headers.fromAddress}&gt;</p>
635
+ <p id="to"><strong>To:</strong> ${headers.toName} &lt;${headers.toAddress}&gt;</p>
636
+ <p id="subject"><strong>Subject:</strong> ${headers.subject}</p>
637
+ </div>
638
+ `;
639
+ const emailContent = headerSection + htmlTemplate;
640
+ return emailContent;
641
+ }
642
+ /**
643
+ * Retrieves the plain text content of the latest email.
644
+ * @returns The plain text content of the latest email.
645
+ */
646
+ async getRenderMessageTxt() {
647
+ const response = await this.context.get("view/latest.txt");
648
+ const buffer = await response.body();
649
+ const text = buffer.toString("utf-8");
650
+ return text;
651
+ }
652
+ /**
653
+ * Extracts the first URL found in the plain text content of the latest email.
654
+ * @returns The first URL found in the email content.
655
+ * @throws An error if no URL is found in the email content.
656
+ */
657
+ async getLinkFromMail() {
658
+ const textContent = await this.getRenderMessageTxt();
659
+ const urlMatch = textContent.match(/https?:\/\/[^\s]+/);
660
+ if (urlMatch && urlMatch.length > 0) {
661
+ return urlMatch[0];
662
+ }
663
+ throw new Error("No URL found in the email content");
664
+ }
665
+ /**
666
+ * Deletes a specific email by ID if provided, or deletes all emails if no ID is provided.
667
+ * @param emailId - The ID of the email to delete (optional).
668
+ */
669
+ async deleteMail(emailId) {
670
+ const data = emailId ? { IDs: [emailId] } : {};
671
+ await this.context.delete(`api/v1/messages`, { data });
672
+ }
673
+ /**
674
+ * Creates a new EmailApiContext instance with the appropriate configuration.
675
+ * @returns A promise that resolves to an EmailApiContext instance.
676
+ */
677
+ static async create(baseURL) {
678
+ const extraHTTPHeaders = {
679
+ "Accept": "application/json",
680
+ "Content-Type": "application/json"
681
+ };
682
+ const context = await request.newContext({
683
+ baseURL,
684
+ ignoreHTTPSErrors: true,
685
+ extraHTTPHeaders
686
+ });
687
+ return new MailpitApiContext(context);
688
+ }
689
+ }
690
+
581
691
  const test$a = test$d.extend({
582
692
  AdminApiContext: [
583
693
  async ({}, use) => {
@@ -597,6 +707,13 @@ const test$a = test$d.extend({
597
707
  await use(storeApiContext);
598
708
  },
599
709
  { scope: "worker" }
710
+ ],
711
+ MailpitApiContext: [
712
+ async ({}, use) => {
713
+ const mailpitApiContext = await MailpitApiContext.create(process.env["MAILPIT_BASE_URL"]);
714
+ await use(mailpitApiContext);
715
+ },
716
+ { scope: "worker" }
600
717
  ]
601
718
  });
602
719
 
@@ -1400,7 +1517,7 @@ class TestDataService {
1400
1517
  */
1401
1518
  async cleanUp() {
1402
1519
  if (!this.shouldCleanUp) {
1403
- return Promise.reject();
1520
+ return null;
1404
1521
  }
1405
1522
  const priorityDeleteOperations = {};
1406
1523
  const deleteOperations = {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shopware-ag/acceptance-test-suite",
3
- "version": "2.3.10",
3
+ "version": "2.4.0",
4
4
  "description": "Shopware Acceptance Test Suite",
5
5
  "author": "shopware AG",
6
6
  "license": "MIT",