From 876d89b6db7cbf9d38e8f0e372d59d49111ffd3a Mon Sep 17 00:00:00 2001 From: wojteknowacki <124166231+wojteknowacki@users.noreply.github.com> Date: Thu, 14 Dec 2023 09:45:55 +0100 Subject: [PATCH] Export products as csv file test (#4521) * test bulk delete on product list * update single product test * test title fix * moved assertions to the end of tests * better selector for grid text * exclude rows eval from loop * print navigation url * Export products as csv test * removed unused log --- .changeset/gentle-glasses-provide.md | 5 ++ playwright/api/mailpit.ts | 82 +++++++++++++------ playwright/data/commonLocators.ts | 1 + playwright/pages/basePage.ts | 19 ++--- .../pages/dialogs/exportProductsDialog.ts | 32 ++++++++ playwright/pages/productPage.ts | 28 ++++++- playwright/tests/product.spec.ts | 39 +++++++-- src/components/Accordion/Accordion.tsx | 4 +- .../ProductExportDialogInfo.tsx | 1 + 9 files changed, 163 insertions(+), 48 deletions(-) create mode 100644 .changeset/gentle-glasses-provide.md create mode 100644 playwright/pages/dialogs/exportProductsDialog.ts diff --git a/.changeset/gentle-glasses-provide.md b/.changeset/gentle-glasses-provide.md new file mode 100644 index 00000000000..d428bcb468c --- /dev/null +++ b/.changeset/gentle-glasses-provide.md @@ -0,0 +1,5 @@ +--- +"saleor-dashboard": minor +--- + +Export products as csv test diff --git a/playwright/api/mailpit.ts b/playwright/api/mailpit.ts index 31f4f80bbfa..c764dc621f3 100644 --- a/playwright/api/mailpit.ts +++ b/playwright/api/mailpit.ts @@ -10,17 +10,23 @@ export class MailpitService { this.request = request; } - async getLastEmails(getEmailsLimit = 100) { + async getLastEmails(getEmailsLimit = 50) { let latestEmails: any; - await expect(async () => { - latestEmails = await this.request.get( - `${mailpitUrl}/api/v1/messages?limit=${getEmailsLimit}`, - ); - expect(latestEmails.body()).not.toBeUndefined(); - }).toPass({ - intervals: [3_000, 3_000, 3_000], - timeout: 10_000, - }); + await expect + .poll( + async () => { + latestEmails = await this.request.get( + `${mailpitUrl}/api/v1/messages?limit=${getEmailsLimit}`, + ); + return latestEmails; + }, + { + message: `Could not found last ${getEmailsLimit}`, + intervals: [2000, 3000, 5000, 5000], + timeout: 15000, + }, + ) + .not.toBeUndefined(); const latestEmailsJson = await latestEmails!.json(); return latestEmailsJson; } @@ -36,22 +42,52 @@ export class MailpitService { async getEmailsForUser(userEmail: string) { let userEmails: any[] = []; - await expect(async () => { - const emails = await this.getLastEmails(); - userEmails = emails.messages.filter((mails: { To: any[] }) => - mails.To.map( - (recipientObj: { Address: any }) => `${recipientObj.Address}`, - ).includes(userEmail), - ); - expect(userEmails.length).toBeGreaterThanOrEqual(1); - }).toPass({ - intervals: [3_000, 3_000, 3_000], - timeout: 10_000, - }); - await expect(userEmails).not.toEqual([]); + await expect + .poll( + async () => { + const emails = await this.getLastEmails(); + userEmails = await emails.messages.filter((mails: { To: any[] }) => + mails.To.map( + (recipientObj: { Address: any }) => `${recipientObj.Address}`, + ).includes(userEmail), + ); + return userEmails.length; + }, + { + message: `User: ${userEmail} messages were not found`, + intervals: [2000, 3000, 5000, 5000], + timeout: 15000, + }, + ) + .toBeGreaterThanOrEqual(1); return userEmails; } + async checkDoesUserReceivedExportedData( + userEmail: string, + mailSubject: string, + ) { + let confirmationMessageReceived: boolean = false; + await expect + .poll( + async () => { + let userEmails: any[] = await this.getEmailsForUser(userEmail); + for (const email of userEmails) { + if (email.Subject === mailSubject) { + confirmationMessageReceived = true; + break; + } + } + return confirmationMessageReceived; + }, + { + message: `Message with subject: ${mailSubject} was not found`, + intervals: [2000, 3000, 5000, 5000], + timeout: 15000, + }, + ) + .toBe(true); + } async generateResetPasswordUrl(userEmail: string) { const tokenRegex = /token=([A-Za-z0-9]+(-[A-Za-z0-9]+)+)/; diff --git a/playwright/data/commonLocators.ts b/playwright/data/commonLocators.ts index cec9ffc569c..23194e24737 100644 --- a/playwright/data/commonLocators.ts +++ b/playwright/data/commonLocators.ts @@ -1,5 +1,6 @@ export const LOCATORS = { successBanner: '[data-test-type="success"]', errorBanner: '[data-test-type="error"]', + infoBanner: '[data-test-type="info"]', dataGridTable: "[data-testid='data-grid-canvas']", }; diff --git a/playwright/pages/basePage.ts b/playwright/pages/basePage.ts index ee9f2da090c..2719210bbe6 100644 --- a/playwright/pages/basePage.ts +++ b/playwright/pages/basePage.ts @@ -1,5 +1,4 @@ import { LOCATORS } from "@data/commonLocators"; -import { URL_LIST } from "@data/url"; import type { Page } from "@playwright/test"; import { expect } from "@playwright/test"; @@ -15,22 +14,10 @@ export class BasePage { .locator("textarea"), readonly successBanner = page.locator(LOCATORS.successBanner), readonly errorBanner = page.locator(LOCATORS.errorBanner), + readonly infoBanner = page.locator(LOCATORS.infoBanner), ) { this.page = page; } - async gotoCreateProductPage(productTypeId: string) { - await this.page.goto( - `${URL_LIST.products}${URL_LIST.productsAdd}${productTypeId}`, - ); - await expect(this.pageHeader).toBeVisible({ timeout: 10000 }); - } - async gotoExistingProductPage(productId: string) { - await console.log( - `Navigating to existing product: ${URL_LIST.products}${productId}`, - ); - await this.page.goto(`${URL_LIST.products}${productId}`); - await expect(this.pageHeader).toBeVisible({ timeout: 10000 }); - } async expectGridToBeAttached() { await expect(this.gridCanvas).toBeAttached({ timeout: 10000, @@ -48,6 +35,10 @@ export class BasePage { .waitFor({ state: "visible", timeout: 15000 }); await expect(this.errorBanner).not.toBeVisible(); } + async expectInfoBanner() { + await this.infoBanner.first().waitFor({ state: "visible", timeout: 15000 }); + await expect(this.errorBanner).not.toBeVisible(); + } async getRandomInt(max: number) { return Math.floor(Math.random() * (max + 1)); diff --git a/playwright/pages/dialogs/exportProductsDialog.ts b/playwright/pages/dialogs/exportProductsDialog.ts new file mode 100644 index 00000000000..fd389fc13c5 --- /dev/null +++ b/playwright/pages/dialogs/exportProductsDialog.ts @@ -0,0 +1,32 @@ +import { Page } from "@playwright/test"; + +export class ExportProductsDialog { + readonly page: Page; + + constructor( + page: Page, + readonly channelsAccordion = page.getByTestId("channel-expand-button"), + readonly nextButton = page.getByTestId("next"), + readonly submitButton = page.getByTestId("submit"), + readonly exportSearchedProductsRadioButton = page.locator( + "input[value='FILTER']", + ), + ) { + this.page = page; + } + async clickChannelsAccordion() { + await this.channelsAccordion.click(); + } + async clickNextButton() { + await this.nextButton.click(); + } + async clickSubmitButton() { + await this.submitButton.click(); + } + async clickExportSearchedProductsRadioButton() { + await this.exportSearchedProductsRadioButton.click(); + } + async checkChannelCheckbox(channel = "PLN") { + await this.page.locator(`[name="Channel-${channel}"]`).click(); + } +} diff --git a/playwright/pages/productPage.ts b/playwright/pages/productPage.ts index 1c6b6045e92..dff0b2268a3 100644 --- a/playwright/pages/productPage.ts +++ b/playwright/pages/productPage.ts @@ -3,9 +3,10 @@ import path from "path"; import { URL_LIST } from "@data/url"; import { ChannelSelectDialog } from "@pages/dialogs/channelSelectDialog"; +import { ExportProductsDialog } from "@pages/dialogs/exportProductsDialog"; import { MetadataSeoPage } from "@pages/pageElements/metadataSeoPage"; import { RightSideDetailsPage } from "@pages/pageElements/rightSideDetailsSection"; -import type { Page } from "@playwright/test"; +import { expect, Page } from "@playwright/test"; import { BasePage } from "./basePage"; import { DeleteProductDialog } from "./dialogs/deleteProductDialog"; @@ -17,6 +18,7 @@ export class ProductPage { readonly page: Page; readonly metadataSeoPage: MetadataSeoPage; + readonly exportProductsDialog: ExportProductsDialog; readonly rightSideDetailsPage: RightSideDetailsPage; readonly basePage: BasePage; readonly channelSelectDialog: ChannelSelectDialog; @@ -29,6 +31,8 @@ export class ProductPage { "product-available-in-channels-text", ), readonly createProductButton = page.getByTestId("add-product"), + readonly cogShowMoreButtonButton = page.getByTestId("show-more-button"), + readonly exportButton = page.getByTestId("export"), readonly bulkDeleteButton = page.getByTestId("bulk-delete-button"), readonly deleteProductButton = page.getByTestId("button-bar-delete"), readonly searchProducts = page.locator( @@ -78,15 +82,37 @@ export class ProductPage { ) { this.page = page; this.basePage = new BasePage(page); + this.exportProductsDialog = new ExportProductsDialog(page); this.deleteProductDialog = new DeleteProductDialog(page); this.channelSelectDialog = new ChannelSelectDialog(page); this.metadataSeoPage = new MetadataSeoPage(page); this.rightSideDetailsPage = new RightSideDetailsPage(page); } + async gotoCreateProductPage(productTypeId: string) { + await this.page.goto( + `${URL_LIST.products}${URL_LIST.productsAdd}${productTypeId}`, + ); + await expect(this.basePage.pageHeader).toBeVisible({ timeout: 10000 }); + } + + async gotoExistingProductPage(productId: string) { + console.log( + `Navigating to existing product: ${URL_LIST.products}${productId}`, + ); + await this.page.goto(`${URL_LIST.products}${productId}`); + await expect(this.basePage.pageHeader).toBeVisible({ timeout: 10000 }); + } + async clickDeleteProductButton() { await this.deleteProductButton.click(); } + async clickExportButton() { + await this.exportButton.click(); + } + async clickCogShowMoreButtonButton() { + await this.cogShowMoreButtonButton.click(); + } async clickUploadImagesButtonButton() { await this.uploadSavedImagesButton.click(); } diff --git a/playwright/tests/product.spec.ts b/playwright/tests/product.spec.ts index 37dbc766262..23efe4431de 100644 --- a/playwright/tests/product.spec.ts +++ b/playwright/tests/product.spec.ts @@ -1,3 +1,4 @@ +import { MailpitService } from "@api/mailpit"; import { PRODUCTS } from "@data/e2eTestData"; import { URL_LIST } from "@data/url"; import { BasePage } from "@pages/basePage"; @@ -32,7 +33,7 @@ test("TC: SALEOR_5 Create basic - single product type - product without variants const basePage = new BasePage(page); const productPage = new ProductPage(page); - await basePage.gotoCreateProductPage(PRODUCTS.singleProductType.id); + await productPage.gotoCreateProductPage(PRODUCTS.singleProductType.id); await productPage.selectOneChannelAsAvailableWhenMoreSelected(); await productPage.typeNameDescAndRating(); await productPage.addSeo(); @@ -49,11 +50,10 @@ test("TC: SALEOR_26 Create basic info variant - via edit variant page @e2e @prod page, }) => { const variantName = `TC: SALEOR_26 - variant name - ${new Date().toISOString()}`; - const basePage = new BasePage(page); const productPage = new ProductPage(page); const variantsPage = new VariantsPage(page); - await basePage.gotoExistingProductPage(PRODUCTS.productWithOneVariant.id); + await productPage.gotoExistingProductPage(PRODUCTS.productWithOneVariant.id); await productPage.clickFirstEditVariantButton(); await variantsPage.clickAddVariantButton(); await variantsPage.typeVariantName(variantName); @@ -75,11 +75,10 @@ test("TC: SALEOR_27 Create full info variant - via edit variant page @e2e @produ page, }) => { const variantName = `TC: SALEOR_27 - variant name - ${new Date().toISOString()}`; - const basePage = new BasePage(page); const productPage = new ProductPage(page); const variantsPage = new VariantsPage(page); - await basePage.gotoExistingProductPage(PRODUCTS.productWithOneVariant.id); + await productPage.gotoExistingProductPage(PRODUCTS.productWithOneVariant.id); await productPage.clickFirstEditVariantButton(); await variantsPage.clickAddVariantButton(); await variantsPage.typeVariantName(variantName); @@ -133,7 +132,7 @@ test("TC: SALEOR_45 As an admin I should be able to delete a single products @ba const basePage = new BasePage(page); const productPage = new ProductPage(page); - await basePage.gotoExistingProductPage( + await productPage.gotoExistingProductPage( PRODUCTS.productWithOneVariantToBeDeletedFromDetails.id, ); await productPage.clickDeleteProductButton(); @@ -143,14 +142,13 @@ test("TC: SALEOR_45 As an admin I should be able to delete a single products @ba PRODUCTS.productWithOneVariantToBeDeletedFromDetails.name, ); }); -test("TC: SALEOR_46 As an admin, I should be able to update a single product by uploading media, assigning channels, assigning tax, and adding a new variant @basic-regression @product @e2e", async ({ +test("TC: SALEOR_46 As an admin, I should be able to update a product by uploading media, assigning channels, assigning tax, and adding a new variant @basic-regression @product @e2e", async ({ page, }) => { const newVariantName = "variant 2"; - const basePage = new BasePage(page); const productPage = new ProductPage(page); - await basePage.gotoExistingProductPage( + await productPage.gotoExistingProductPage( PRODUCTS.singleProductTypeToBeUpdated.id, ); await productPage.clickUploadMediaButton(); @@ -182,3 +180,26 @@ test("TC: SALEOR_46 As an admin, I should be able to update a single product by ); expect(await productPage.productImage.count()).toEqual(1); }); + +// blocked by bug https://github.com/saleor/saleor-dashboard/issues/4368 +test.skip("TC: SALEOR_56 As an admin, I should be able to export products from single channel as CSV file @basic-regression @product @e2e", async ({ + page, + request, +}) => { + const productPage = new ProductPage(page); + const mailpitService = new MailpitService(request); + + await page.goto(URL_LIST.products); + await productPage.clickCogShowMoreButtonButton(); + await productPage.clickExportButton(); + await productPage.exportProductsDialog.clickChannelsAccordion(); + await productPage.exportProductsDialog.checkChannelCheckbox("PLN"); + await productPage.exportProductsDialog.clickNextButton(); + await productPage.exportProductsDialog.clickExportSearchedProductsRadioButton(); + await productPage.exportProductsDialog.clickSubmitButton(); + await productPage.basePage.expectInfoBanner(); + const userEmails = await mailpitService.checkDoesUserReceivedExportedData( + process.env.E2E_USER_NAME!, + "Your exported products data is ready", + ); +}); diff --git a/src/components/Accordion/Accordion.tsx b/src/components/Accordion/Accordion.tsx index e09c5298958..dc324578f74 100644 --- a/src/components/Accordion/Accordion.tsx +++ b/src/components/Accordion/Accordion.tsx @@ -12,6 +12,7 @@ export interface AccordionProps { initialExpand?: boolean; quickPeek?: React.ReactNode; title: string; + dataTestId?: string; } const AccordionItemId = "accordionItemId"; @@ -22,6 +23,7 @@ const Accordion: React.FC = ({ quickPeek, title, className, + dataTestId = "expand-icon", }) => { const [openedAccordionId, setOpendAccordionId] = useState( !!initialExpand ? AccordionItemId : undefined, @@ -45,7 +47,7 @@ const Accordion: React.FC = ({ {title} - + diff --git a/src/products/components/ProductExportDialog/ProductExportDialogInfo.tsx b/src/products/components/ProductExportDialog/ProductExportDialogInfo.tsx index b0f49f36803..1e292bc723c 100644 --- a/src/products/components/ProductExportDialog/ProductExportDialogInfo.tsx +++ b/src/products/components/ProductExportDialog/ProductExportDialogInfo.tsx @@ -320,6 +320,7 @@ const ProductExportDialogInfo: React.FC = ({