Merge branch 'main' into envs

This commit is contained in:
Lukasz Ostrowski 2023-07-03 11:21:17 +02:00
commit e7a3df4777
39 changed files with 940 additions and 194 deletions

View file

@ -0,0 +1,14 @@
---
"saleor-app-emails-and-messages": minor
"saleor-app-data-importer": minor
"saleor-app-products-feed": minor
"saleor-app-invoices": minor
"saleor-app-klaviyo": minor
"saleor-app-search": minor
"saleor-app-slack": minor
"saleor-app-taxes": minor
"saleor-app-cms": minor
"saleor-app-crm": minor
---
Filled "about" field in App Manifest. Dashboard will display it in app details page now.

View file

@ -0,0 +1,17 @@
---
"saleor-app-emails-and-messages": patch
"saleor-app-data-importer": patch
"saleor-app-products-feed": patch
"saleor-app-monitoring": patch
"@saleor/apps-shared": patch
"saleor-app-invoices": patch
"saleor-app-klaviyo": patch
"saleor-app-search": patch
"@saleor/apps-ui": patch
"saleor-app-slack": patch
"saleor-app-taxes": patch
"saleor-app-cms": patch
"saleor-app-crm": patch
---
Update @saleor/app-sdk to 0.41.0

View file

@ -13,6 +13,8 @@ export default createManifestHandler({
const apiBaseURL = process.env.APP_API_BASE_URL ?? appBaseUrl;
const manifest: AppManifest = {
about:
"CMS App is a multi-integration app that connects Saleor with popular Content Management Systems.",
appUrl: iframeBaseUrl,
author: "Saleor Commerce",
brand: {

View file

@ -11,6 +11,7 @@ export default createManifestHandler({
const apiBaseURL = process.env.APP_API_BASE_URL ?? appBaseUrl;
const manifest: AppManifest = {
about: "CRM App allows synchronization of customers from Saleor to other platforms",
appUrl: iframeBaseUrl,
author: "Saleor Commerce",
brand: {

View file

@ -9,6 +9,8 @@ export default createManifestHandler({
const apiBaseURL = process.env.APP_API_BASE_URL ?? appBaseUrl;
const manifest: AppManifest = {
about:
"Data Importer allows batch import of shop data to Saleor from sources like CSV or Excel",
appUrl: iframeBaseUrl,
author: "Saleor Commerce",
brand: {

View file

@ -9,6 +9,8 @@ export default createManifestHandler({
const apiBaseURL = process.env.APP_API_BASE_URL ?? appBaseUrl;
const manifest: AppManifest = {
about:
"Emails & Messages App is a multi-vendor Saleor app that integrates with notification services.",
appUrl: iframeBaseUrl,
author: "Saleor Commerce",
brand: {

View file

@ -11,6 +11,8 @@ export default createManifestHandler({
const apiBaseURL = process.env.APP_API_BASE_URL ?? appBaseUrl;
const manifest: AppManifest = {
about:
"An app that generates PDF invoices for Orders and stores them in Saleor file storage.",
appUrl: iframeBaseUrl,
author: "Saleor Commerce",
brand: {

View file

@ -15,6 +15,7 @@ const handler = createManifestHandler({
const apiBaseURL = process.env.APP_API_BASE_URL ?? appBaseUrl;
return {
about: "Klaviyo integration allows sending Klaviyo notifications on Saleor events.",
appUrl: iframeBaseUrl,
author: "Saleor Commerce",
brand: {

View file

@ -14,7 +14,7 @@
"@material-ui/core": "^4.12.4",
"@material-ui/icons": "^4.11.3",
"@material-ui/lab": "4.0.0-alpha.61",
"@saleor/app-sdk": "0.40.1",
"@saleor/app-sdk": "0.41.0",
"@saleor/apps-shared": "workspace:*",
"@saleor/apps-ui": "workspace:*",
"@saleor/macaw-ui": "0.8.0-pre.95",

View file

@ -14,6 +14,7 @@ export default createManifestHandler({
const apiBaseURL = process.env.APP_API_BASE_URL ?? appBaseUrl;
const manifest: AppManifest = {
about: "Generate feeds consumed by Merchant Platforms",
appUrl: iframeBaseUrl,
author: "Saleor Commerce",
brand: {

View file

@ -15,7 +15,10 @@ export default createManifestHandler({
const apiBaseURL = process.env.APP_API_BASE_URL ?? appBaseUrl;
const manifest: AppManifest = {
about:
"Search App is a multi-integration app that connects your Saleor store with search engines.",
appUrl: iframeBaseUrl,
author: "Saleor Commerce",
brand: {
logo: {
default: `${apiBaseURL}/logo.png`,

View file

@ -10,6 +10,8 @@ const handler = createManifestHandler({
const apiBaseURL = process.env.APP_API_BASE_URL ?? appBaseUrl;
const manifest: AppManifest = {
about:
"Saleor Slack integration allows you to get notifications on Slack channel from Saleor events.",
appUrl: iframeBaseUrl,
author: "Saleor Commerce",
brand: {

View file

@ -14,6 +14,7 @@ export default createManifestHandler({
const apiBaseURL = process.env.APP_API_BASE_URL ?? appBaseUrl;
const manifest: AppManifest = {
about: "Taxes App allows dynamic taxes calculations for orders",
appUrl: iframeBaseUrl,
author: "Saleor Commerce",
brand: {

View file

@ -0,0 +1,3 @@
INSTANCE_URL=
DASHBOARD_USER_EMAIL=
DASHBOARD_USER_PASSWORD=

4
packages/e2e/.eslintrc Normal file
View file

@ -0,0 +1,4 @@
{
"root": true,
"extends": ["saleor"]
}

3
packages/e2e/.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/test-results/
/playwright-report/
/playwright/.cache/

20
packages/e2e/package.json Normal file
View file

@ -0,0 +1,20 @@
{
"name": "@saleor/e2e",
"description": "",
"version": "1.0.0",
"author": "",
"scripts": {
"e2e": "playwright test",
"e2e:ui": "playwright test --ui"
},
"devDependencies": {
"@playwright/test": "^1.35.1",
"@saleor/app-sdk": "0.40.1",
"dotenv": "^16.3.1",
"eslint-config-saleor": "workspace:*",
"zod": "3.20.2"
},
"keywords": [],
"license": "ISC",
"main": "index.js"
}

View file

@ -0,0 +1,62 @@
import { defineConfig, devices } from "@playwright/test";
/**
* Read environment variables from file.
* Check setup/configuration
*/
require("dotenv").config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
// eslint-disable-next-line import/no-default-export
export default defineConfig({
testDir: "./tests",
/* Run tests in files in parallel */
fullyParallel: false,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
// reporter: "html",
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: "on-first-retry",
},
/* Configure projects for major browsers */
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
/*
*
* {
* name: 'firefox',
* use: { ...devices['Desktop Firefox'] },
* },
*
* {
* name: 'webkit',
* use: { ...devices['Desktop Safari'] },
* },
*/
],
/* Run your local dev server before starting the tests */
/*
* webServer: {
* command: 'npm run start',
* url: 'http://127.0.0.1:3000',
* reuseExistingServer: !process.env.CI,
* },
*/
});

View file

@ -0,0 +1,17 @@
import { z } from "zod";
const instanceUrl = process.env.INSTANCE_URL;
const dashboardUserEmail = process.env.DASHBOARD_USER_EMAIL;
const dashboardUserPassword = process.env.DASHBOARD_USER_PASSWORD;
export const configuration = z
.object({
instanceUrl: z.string().nonempty().url(),
dashboardUserEmail: z.string().nonempty(),
dashboardUserPassword: z.string().nonempty(),
})
.parse({
instanceUrl,
dashboardUserEmail,
dashboardUserPassword,
});

View file

@ -0,0 +1,20 @@
import { configuration } from "./configuration";
export const appUrls = (appUrl: string) => ({
manifest: new URL("/api/manifest", appUrl).href,
register: new URL("/api/register", appUrl).href,
});
const saleorUrls = (dashboardUrl: string) => ({
dashboard: {
homepage: new URL("/dashboard", dashboardUrl).href,
apps: new URL("/dashboard/apps", dashboardUrl).href,
appInstallPage: (appManifest: string) =>
new URL(`/dashboard/apps/install?manifestUrl=${appManifest}`, dashboardUrl).href,
},
api: new URL("/graphql/", dashboardUrl).href,
});
export const routing = {
saleor: saleorUrls(configuration.instanceUrl),
};

View file

@ -0,0 +1,66 @@
import { test, expect, Page } from "@playwright/test";
import { logInIntoDashboard } from "./operations/log-in-to-dashboard";
import { installTheApp } from "./operations/install-app";
import { appUrls, routing } from "../setup/routing";
import { AppManifest } from "@saleor/app-sdk/types";
import { assertAppAvailable } from "./assertions/assert-app-available";
/**
* Hardcoded list of every app deployed on staging and production.
* TODO: Eventually this should be the entry point and the list should be provided via env
*/
const apps: string[] = [
"taxes",
"crm",
"cms",
"emails-and-messages",
"product-feed",
"search",
"klaviyo",
"slack",
"invoices",
"data-importer",
].reduce((urls, appSegment) => {
urls.push(`https://${appSegment}.saleor.app`);
urls.push(`https://${appSegment}.staging.saleor.app`);
return urls;
}, []);
/*
*
* test.describe.configure({
* mode: "parallel",
* });
*/
/**
* TODO Enable parallel mode. It cant work with beforeAll.
*/
test.describe("Apps Installation", () => {
let page: Page;
test.beforeAll(async ({ browser }) => {
if (page) {
return;
}
console.log("beforeAll run");
page = await browser.newPage();
await logInIntoDashboard({ page });
});
for (const appUrl of apps) {
test(`App: "${appUrl}" can be installed in the dashboard`, async () => {
const appManifestUrl = appUrl + "/api/manifest";
await installTheApp({ page, appManifest: appManifestUrl }); // todo extract to helper
const appManifest = (await fetch(appManifestUrl).then((r) => r.json())) as AppManifest;
const appName = appManifest.name;
await assertAppAvailable({ page, appName });
});
}
});

View file

@ -0,0 +1,33 @@
import { Page, test, expect } from "@playwright/test";
import { logInIntoDashboard } from "../../operations/log-in-to-dashboard";
import { openTheApp } from "../../operations/open-app";
test.describe("Klaviyo Configuration", () => {
let page: Page;
test.beforeAll(async ({ browser }) => {
if (page) {
return;
}
console.log("beforeAll run");
page = await browser.newPage();
await logInIntoDashboard({ page });
});
// Test assumes app is installed
test("App can be configured @stable @critical", async () => {
await openTheApp({ page, appName: "Klaviyo" });
// todo make more strict selector
const iframeLocator = page.frameLocator("iframe");
await expect(iframeLocator.getByLabel("CUSTOMER_CREATED_METRIC")).toBeVisible();
await expect(iframeLocator.getByLabel("FULFILLMENT_CREATED_METRIC")).toBeVisible();
await expect(iframeLocator.getByLabel("ORDER_CREATED_METRIC")).toBeVisible();
await expect(iframeLocator.getByLabel("ORDER_FULLY_PAID_METRIC")).toBeVisible();
await expect(iframeLocator.getByLabel("PUBLIC_TOKEN")).toBeVisible();
});
});

View file

@ -0,0 +1,8 @@
import { expect, FrameLocator } from "@playwright/test";
export const assertAppRender = async (iframeLocator: FrameLocator) => {
await expect(iframeLocator.getByTestId("root-heading")).toBeVisible();
await expect(iframeLocator.getByTestId("s3-configuration-section")).toBeVisible();
await expect(iframeLocator.getByTestId("channels-configuration-section")).toBeVisible();
await expect(iframeLocator.getByTestId("categories-mapping-section")).toBeVisible();
};

View file

@ -0,0 +1,10 @@
import { FrameLocator, Locator } from "@playwright/test";
export const fillAwsS3Form = async (iframeLocator: FrameLocator) => {
await iframeLocator.getByLabel("Amazon access key ID").fill("test-id");
await iframeLocator.getByLabel("Amazon secret access key").fill("test-secret");
await iframeLocator.getByLabel("Bucket name").fill("test-bucket");
await iframeLocator.getByLabel("Bucket region").fill("eu-west-1");
await iframeLocator.getByText("Save bucket configuration").click();
};

View file

@ -0,0 +1,16 @@
import { FrameLocator } from "@playwright/test";
export const fillChannelConfig = async (iframeLocator: FrameLocator) => {
const sectionsSelector = await iframeLocator.getByTestId("channels-configuration-section");
const channelRow = sectionsSelector.getByText("Default channel"); // todo add test-id
channelRow.click();
await sectionsSelector.getByLabel("Storefront URL").fill("https://www.example.com");
await sectionsSelector
.getByLabel("Storefront product URL")
.fill("https://www.example.com/{productId}");
await sectionsSelector.getByText("Save channel settings").click();
};

View file

@ -0,0 +1,12 @@
import { FrameLocator, expect } from "@playwright/test";
export const navigateToCategoryMapping = async (iframeLocator: FrameLocator) => {
await iframeLocator.getByText("Map categories").click({ force: true });
await expect(iframeLocator.getByTestId("categories-mapping-container")).toBeVisible();
// todo doesnt load, probably app must be optimized
await expect(iframeLocator.getByText("Accessories")).toBeVisible({
timeout: 120000,
});
};

View file

@ -0,0 +1,7 @@
import { FrameLocator } from "@playwright/test";
export const setCategoryMapping = async (iframeLocator: FrameLocator) => {
await iframeLocator.locator("select").first().selectOption({ index: 0 });
await iframeLocator.getByText("Save").first().click();
};

View file

@ -0,0 +1,63 @@
import { Page, test, expect } from "@playwright/test";
import { logInIntoDashboard } from "../../operations/log-in-to-dashboard";
import { openTheApp } from "../../operations/open-app";
import { fillAwsS3Form } from "./operations/fill-aws-s3-form";
import { assertAppRender } from "./assertions/assert-app-render";
import { fillChannelConfig } from "./operations/fill-channel-config";
import { setCategoryMapping } from "./operations/set-category-mapping";
import { navigateToCategoryMapping } from "./operations/navigate-to-category-mapping";
test.describe("Product Feed Configuration", () => {
let page: Page;
test.beforeAll(async ({ browser }) => {
if (page) {
return;
}
console.log("beforeAll run");
page = await browser.newPage();
await logInIntoDashboard({ page });
});
// Test assumes app is installed
test("App can be configured @stable @critical", async () => {
await openTheApp({ page, appName: "Product Feed" });
// todo make more strict selector
const iframeLocator = page.frameLocator("iframe");
await assertAppRender(iframeLocator);
await fillAwsS3Form(iframeLocator);
await expect(page.getByText("Updated S3 configuration")).toBeVisible({ timeout: 10000 });
await fillChannelConfig(iframeLocator);
await expect(page.getByText("Success")).toBeVisible({
timeout: 10000,
}); // todo add more meaningul message, only "success" is set
});
/**
* Test fails. Probably because of a very big list of Google categories that are fetched.
* TODO: Fix this in the app
*/
test.skip("App can be configured with categories mapping", async () => {
await openTheApp({ page, appName: "Product Feed" });
// todo make more strict selector
const iframeLocator = page.frameLocator("iframe");
await navigateToCategoryMapping(iframeLocator);
await setCategoryMapping(iframeLocator);
await expect(page.getByText("Success")).toBeVisible({ timeout: 10000 }); // todo add more meaningul message, only "success" is set
await iframeLocator.getByText("Configuration").click();
});
});

View file

@ -0,0 +1,21 @@
import { expect, FrameLocator } from "@playwright/test";
export const assertAppRender = async (iframeLocator: FrameLocator) => {
/*
* TODO Add test-ids assertions after added to app
* todo assert empty state, but these tests must ensure app has fresh install
*/
await expect(
iframeLocator.getByRole("heading", {
name: "Tax providers",
})
).toBeVisible();
await expect(iframeLocator.getByRole("heading", { name: "Available channels" })).toBeVisible();
// await expect(iframeLocator.getByRole("heading", { name: "Tax code matcher" })).toBeVisible(); // todo enable when app enables
const addProviderButton = await iframeLocator.getByRole("button", {
name: new RegExp(/Add new|Add first provider/),
});
await expect(addProviderButton).toBeVisible();
};

View file

@ -0,0 +1,57 @@
import { Page, test, expect } from "@playwright/test";
import { logInIntoDashboard } from "../../operations/log-in-to-dashboard";
import { openTheApp } from "../../operations/open-app";
import { assertAppRender } from "./assertions/assert-app-render";
// Test assumes app is installed
test.describe("Taxes Configuration", () => {
let page: Page;
test.beforeAll(async ({ browser }) => {
if (page) {
return;
}
page = await browser.newPage();
await logInIntoDashboard({ page });
});
test("App renders config screen @stable @critical", async () => {
await openTheApp({ page, appName: "Taxes" });
// todo make more strict selector
const iframeLocator = page.frameLocator("iframe");
await assertAppRender(iframeLocator);
});
test("App can configure new Taxjar provider @taxjar", async () => {
await openTheApp({ page, appName: "Taxes" });
// todo make more strict selector
const iframeLocator = page.frameLocator("iframe");
await iframeLocator
.getByRole("button", {
name: new RegExp(/Add new|Add first provider/),
})
.click();
await iframeLocator.getByRole("button", { name: "Choose" }).first().click(); // todo - test id
await iframeLocator.getByLabel("Configuration name").fill("Test Taxjar provider");
await iframeLocator.getByLabel("API key").fill("TEST");
await iframeLocator.getByLabel("Street").fill("Street");
await iframeLocator.getByLabel("City").fill("City");
await iframeLocator.getByLabel("State").fill("State");
await iframeLocator.getByRole("combobox", { name: "Country *" }).click();
await iframeLocator.getByText("Albania").click();
await iframeLocator.getByLabel("Zip").fill("Zip");
await iframeLocator.getByRole("button", { name: "Save" }).first().click(); // todo - test id
});
// todo
test.skip("App can configure new Avalara provider @avalara", async () => {});
});

View file

@ -0,0 +1,19 @@
import { expect, Page } from "@playwright/test";
import { routing } from "../../setup/routing";
import { configuration } from "../../setup/configuration";
interface checkIfAppIsAvailableArgs {
page: Page;
appName: string;
}
export const assertAppAvailable = async ({ page, appName }: checkIfAppIsAvailableArgs) => {
await page.goto(routing.saleor.dashboard.apps, { timeout: 20000, waitUntil: "load" });
// todo need data-testid. this element is not unique
const appEntry = await page.getByText(appName).first();
await expect(appEntry).toBeVisible();
await expect(await page.getByText("Problem occured during installation.")).toBeHidden();
};

View file

@ -0,0 +1,22 @@
import { Page } from "@playwright/test";
import { routing } from "../../setup/routing";
interface InstallTheAppArgs {
page: Page;
appManifest: string;
}
export const installTheApp = async ({ page, appManifest }: InstallTheAppArgs) => {
// got to Apps page, assuming user is logged in
await page.goto(routing.saleor.dashboard.appInstallPage(appManifest), {
timeout: 20000,
waitUntil: "load",
});
console.log("Navigated to", page.url());
await page.getByRole("button", { name: "Install App" }).click();
// wait for the toast
await page.getByText("App installed").isVisible();
};

View file

@ -0,0 +1,25 @@
import { Page, expect } from "@playwright/test";
import { configuration } from "../../setup/configuration";
import { routing } from "../../setup/routing";
interface LogInIntoDashboardArgs {
page: Page;
}
export const logInIntoDashboard = async ({ page }: LogInIntoDashboardArgs) => {
console.log("Will redirect to", routing.saleor.dashboard.homepage);
await page.goto(routing.saleor.dashboard.homepage, { timeout: 20000, waitUntil: "load" });
const url = page.url();
await page.locator('[data-test-id="email"]').click();
await page.locator('[data-test-id="email"]').fill(configuration.dashboardUserEmail);
await page.locator('[data-test-id="email"]').press("Tab");
await page.locator('[data-test-id="password"]').fill(configuration.dashboardUserPassword);
await page.locator('[data-test-id="submit"]').click();
await expect(page.locator('[data-test-id="welcome-header"]')).toBeVisible();
console.log("Logged in");
};

View file

@ -0,0 +1,21 @@
import { Page, expect } from "@playwright/test";
import { routing } from "../../setup/routing";
interface InstallTheAppArgs {
page: Page;
appName: string;
}
export const openTheApp = async ({ page, appName }: InstallTheAppArgs) => {
// got to Apps page, assuming user is logged in
await page.goto(routing.saleor.dashboard.apps, {
waitUntil: "load",
});
console.log("Navigated to", page.url());
// todo it must be test-id because we also have same name in appstore list
await page.getByText(appName).first().click();
await expect(page.url()).toMatch(new RegExp("https:\\/\\/.*\\/dashboard\\/apps\\/.*\\/app"));
};

12
packages/e2e/turbo.json Normal file
View file

@ -0,0 +1,12 @@
{
"$schema": "https://turbo.build/schema.json",
"extends": ["//"],
"pipeline": {
"e2e": {
"env": ["INSTANCE_URL", "DASHBOARD_USER_PASSWORD", "DASHBOARD_USER_EMAIL"]
},
"e2e:ui": {
"env": ["INSTANCE_URL", "DASHBOARD_USER_PASSWORD", "DASHBOARD_USER_EMAIL"]
}
}
}

View file

@ -17,7 +17,7 @@
"@material-ui/core": "^4.12.4",
"@material-ui/icons": "^4.11.3",
"@material-ui/lab": "4.0.0-alpha.61",
"@saleor/app-sdk": "0.40.1",
"@saleor/app-sdk": "0.41.0",
"@saleor/macaw-ui": "^0.7.2",
"@types/react": "18.2.5",
"@types/react-dom": "18.2.5",

View file

@ -9,7 +9,7 @@
"typescript": "5.1.3"
},
"devDependencies": {
"@saleor/app-sdk": "0.40.1",
"@saleor/app-sdk": "0.41.0",
"@saleor/macaw-ui": "0.8.0-pre.95",
"@types/react": "18.2.5",
"@types/react-dom": "18.2.5",

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"globalEnv": ["VERCEL_ENV", "APP_LOG_LEVEL", "NODE_ENV"],
"globalEnv": ["VERCEL_ENV", "APP_LOG_LEVEL", "NODE_ENV", "CI"],
"pipeline": {
"build": {
"env": ["NEXT_PUBLIC_VERCEL_ENV"],