web: rewrite the whole test suite to be more resilient
**Why?** The old test suite was a confusing mess, hard to maintain, update, and add more tests to. It lacked a much needed layer of expressivity & the reusable functions were all over the place. Since it used a global `page` (by mistake), it couldn't run in parallel. Moreover, the global `page` approach caused random flakiness. All the above reasons led to this OM (Object Model) based approach to tests. The tests are now much more expressive, reslient, resuable & easier to maintain. During the rewriting process I also added a couple more tests (about 10) so this is a net improvement. Previously, running the tests were also quite slow (15-25s avg). This has now been improved to (5-8s avg) by running the tests in production. This means the app now requires to be built before running the tests: ```sh npm run build:test:web # this is only required once npm run test:web ```
@@ -1,250 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2022 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const { test, expect } = require("@playwright/test");
|
||||
const { getTestId, loginUser } = require("./utils");
|
||||
|
||||
test.setTimeout(45 * 1000);
|
||||
/**
|
||||
* @type {import('@playwright/test').Page}
|
||||
*/
|
||||
var page = null;
|
||||
global.page = null;
|
||||
|
||||
test.describe.configure({ mode: "serial" });
|
||||
|
||||
test.beforeAll(async ({ browser, baseURL }) => {
|
||||
// Create page yourself and sign in.
|
||||
const _page = await browser.newPage();
|
||||
global.page = _page;
|
||||
page = _page;
|
||||
|
||||
await page.goto(baseURL);
|
||||
await page.waitForSelector(getTestId("routeHeader"));
|
||||
|
||||
await loginUser();
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await page.close();
|
||||
});
|
||||
|
||||
const plans = [
|
||||
{ key: "monthly", title: "Monthly", coupon: "INTRO50" },
|
||||
{ key: "yearly", title: "Yearly", coupon: "YEAR15" }
|
||||
];
|
||||
const pricingItems = ["subtotal", "tax", "discount", "total"];
|
||||
|
||||
function getPaddleTestId(id) {
|
||||
return `[data-testid="${id}"]`;
|
||||
}
|
||||
|
||||
async function getPrices() {
|
||||
let prices = [];
|
||||
for (let item of pricingItems) {
|
||||
const price = await page.innerText(getTestId(`checkout-price-${item}`));
|
||||
prices.push(`${item}: ${price}`);
|
||||
}
|
||||
return prices;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} prices
|
||||
*/
|
||||
function roundOffPrices(prices) {
|
||||
return prices.replaceAll(/(\d+.\d+)/gm, (str, price) => {
|
||||
const digits = "0".repeat(price.length - 1);
|
||||
return isNaN(parseFloat(price)) ? 0 : `${price[0]}${digits}`;
|
||||
// Math.ceil(Math.round(price) / 10) * 10;
|
||||
});
|
||||
}
|
||||
|
||||
async function getPaddleFrame(selector = "inlineComplianceBarContainer") {
|
||||
await page.waitForSelector(`.checkout-container iframe`);
|
||||
|
||||
const paddleFrame = await (
|
||||
await page.$(".checkout-container iframe")
|
||||
).contentFrame();
|
||||
await paddleFrame.waitForSelector(getPaddleTestId(selector));
|
||||
|
||||
return paddleFrame;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {Page} page
|
||||
* @param {*} paddleFrame
|
||||
* @param {*} countryCode
|
||||
* @param {*} pinCode
|
||||
*/
|
||||
async function changeCountry(page, paddleFrame, countryCode, pinCode) {
|
||||
await paddleFrame.selectOption(
|
||||
getPaddleTestId("countriesSelect"),
|
||||
countryCode,
|
||||
{ force: true }
|
||||
);
|
||||
|
||||
if (pinCode)
|
||||
await paddleFrame.fill(getPaddleTestId("postcodeInput"), pinCode);
|
||||
|
||||
await paddleFrame.click(getPaddleTestId("locationFormSubmitButton"));
|
||||
|
||||
await paddleFrame.waitForSelector(
|
||||
getPaddleTestId("inlineComplianceBarContainer")
|
||||
);
|
||||
|
||||
await page.waitForSelector(
|
||||
getTestId(`checkout-plan-country-${countryCode}`),
|
||||
{ state: "attached" }
|
||||
);
|
||||
}
|
||||
|
||||
async function forEachPlan(action) {
|
||||
for (let i = 0; i < plans.length; ++i) {
|
||||
const plan = plans[i];
|
||||
|
||||
const planTestId = getTestId(`checkout-plan-${plan.key}`);
|
||||
await page.isEnabled(planTestId, { timeout: 10000 });
|
||||
|
||||
await page.click(planTestId);
|
||||
|
||||
await action(plan);
|
||||
|
||||
if (i < plans.length - 1) {
|
||||
await page.click(getTestId("checkout-plan-change"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test("change plans", async () => {
|
||||
await page.goto("/notes/#/buy/");
|
||||
|
||||
await page.waitForSelector(getTestId("see-all-plans"));
|
||||
|
||||
await page.click(getTestId("see-all-plans"));
|
||||
|
||||
await forEachPlan(async (plan) => {
|
||||
await expect(
|
||||
page.innerText(getTestId("checkout-plan-title"))
|
||||
).resolves.toBe(plan.title);
|
||||
});
|
||||
});
|
||||
|
||||
test("confirm plan prices", async () => {
|
||||
await page.goto("/#/buy/");
|
||||
|
||||
await page.waitForSelector(getTestId("see-all-plans"));
|
||||
|
||||
await page.click(getTestId("see-all-plans"));
|
||||
|
||||
await forEachPlan(async (plan) => {
|
||||
const prices = roundOffPrices((await getPrices()).join("\n"));
|
||||
expect(prices).toMatchSnapshot(`checkout-${plan.key}-prices.txt`);
|
||||
});
|
||||
});
|
||||
|
||||
test("changing locale should show localized prices", async () => {
|
||||
await page.goto("/#/buy/");
|
||||
|
||||
await page.waitForSelector(getTestId("see-all-plans"));
|
||||
|
||||
await page.click(getTestId("see-all-plans"));
|
||||
|
||||
await forEachPlan(async (plan) => {
|
||||
const paddleFrame = await getPaddleFrame();
|
||||
|
||||
await changeCountry(page, paddleFrame, "IN", "110001");
|
||||
|
||||
const prices = roundOffPrices((await getPrices()).join("\n"));
|
||||
expect(prices).toMatchSnapshot(`checkout-${plan.key}-IN-prices.txt`);
|
||||
});
|
||||
});
|
||||
|
||||
test("applying coupon should change discount & total price", async () => {
|
||||
await page.goto("/#/buy/");
|
||||
|
||||
await page.waitForSelector(getTestId("see-all-plans"));
|
||||
|
||||
await page.click(getTestId("see-all-plans"));
|
||||
|
||||
await forEachPlan(async (plan) => {
|
||||
await getPaddleFrame();
|
||||
|
||||
await page.fill(getTestId("checkout-coupon-code"), plan.coupon);
|
||||
await page.press(getTestId("checkout-coupon-code"), "Enter");
|
||||
|
||||
await getPaddleFrame();
|
||||
|
||||
await page.waitForSelector(getTestId(`checkout-plan-coupon-applied`), {
|
||||
state: "attached"
|
||||
});
|
||||
|
||||
const prices = roundOffPrices((await getPrices()).join("\n"));
|
||||
|
||||
expect(prices).toMatchSnapshot(
|
||||
`checkout-${plan.key}-discounted-prices.txt`
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("apply coupon through url", async () => {
|
||||
for (let plan of plans) {
|
||||
await page.goto(`/#/buy/${plan.key}/${plan.coupon}`);
|
||||
|
||||
await getPaddleFrame();
|
||||
|
||||
await page.waitForSelector(getTestId(`checkout-plan-coupon-applied`), {
|
||||
state: "attached"
|
||||
});
|
||||
|
||||
const prices = roundOffPrices((await getPrices()).join("\n"));
|
||||
expect(prices).toMatchSnapshot(
|
||||
`checkout-${plan.key}-discounted-prices.txt`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("apply coupon after changing country", async () => {
|
||||
await page.goto("/#/buy/");
|
||||
|
||||
await page.waitForSelector(getTestId("see-all-plans"));
|
||||
|
||||
await page.click(getTestId("see-all-plans"));
|
||||
|
||||
await forEachPlan(async (plan) => {
|
||||
const paddleFrame = await getPaddleFrame();
|
||||
|
||||
await changeCountry(page, paddleFrame, "IN", "110001");
|
||||
|
||||
await page.fill(getTestId("checkout-coupon-code"), plan.coupon);
|
||||
await page.press(getTestId("checkout-coupon-code"), "Enter");
|
||||
|
||||
await getPaddleFrame();
|
||||
|
||||
await page.waitForSelector(getTestId(`checkout-plan-coupon-applied`), {
|
||||
state: "attached"
|
||||
});
|
||||
|
||||
const prices = roundOffPrices((await getPrices()).join("\n"));
|
||||
expect(prices).toMatchSnapshot(
|
||||
`checkout-${plan.key}-IN-discounted-prices.txt`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,5 +0,0 @@
|
||||
subtotal: ₹300000
|
||||
tax: ₹60000
|
||||
discount: -₹200000
|
||||
total: ₹200000
|
||||
first month then ₹400000/mo
|
||||
@@ -1,5 +0,0 @@
|
||||
subtotal: ₹300000
|
||||
tax: ₹60000
|
||||
discount: -₹200000
|
||||
total: ₹200000
|
||||
first month then ₹400000/mo
|
||||
@@ -1,5 +0,0 @@
|
||||
subtotal: ₹300000
|
||||
tax: ₹60000
|
||||
discount: -₹200000
|
||||
total: ₹200000
|
||||
first month then ₹400000/mo
|
||||
@@ -1,5 +0,0 @@
|
||||
subtotal: ₹300000
|
||||
tax: ₹60000
|
||||
discount: ₹0000
|
||||
total: ₹400000/mo
|
||||
forever
|
||||
@@ -1,5 +0,0 @@
|
||||
subtotal: ₹300000
|
||||
tax: ₹60000
|
||||
discount: ₹0000
|
||||
total: ₹400000/mo
|
||||
forever
|
||||
@@ -1,5 +0,0 @@
|
||||
subtotal: ₹300000
|
||||
tax: ₹60000
|
||||
discount: ₹0000
|
||||
total: ₹400000/mo
|
||||
forever
|
||||
@@ -1,5 +0,0 @@
|
||||
subtotal: $4000
|
||||
tax: $0000
|
||||
discount: -$2000
|
||||
total: $2000
|
||||
first month then $4000/mo
|
||||
@@ -1,5 +0,0 @@
|
||||
subtotal: $4000
|
||||
tax: $0000
|
||||
discount: -$2000
|
||||
total: $2000
|
||||
first month then $4000/mo
|
||||
@@ -1,5 +0,0 @@
|
||||
subtotal: $4000
|
||||
tax: $0000
|
||||
discount: -$2000
|
||||
total: $2000
|
||||
first month then $4000/mo
|
||||
@@ -1,5 +0,0 @@
|
||||
subtotal: $4000
|
||||
tax: $0000
|
||||
discount: $0000
|
||||
total: $4000/mo
|
||||
forever
|
||||
@@ -1,5 +0,0 @@
|
||||
subtotal: $4000
|
||||
tax: $0000
|
||||
discount: $0000
|
||||
total: $4000/mo
|
||||
forever
|
||||
@@ -1,5 +0,0 @@
|
||||
subtotal: $4000
|
||||
tax: $0000
|
||||
discount: $0000
|
||||
total: $4000/mo
|
||||
forever
|
||||
@@ -1,5 +0,0 @@
|
||||
subtotal: ₹4000000
|
||||
tax: ₹700000
|
||||
discount: -₹700000
|
||||
total: ₹4000000
|
||||
first year then ₹4000000/yr
|
||||
@@ -1,5 +0,0 @@
|
||||
subtotal: ₹4000000
|
||||
tax: ₹700000
|
||||
discount: -₹700000
|
||||
total: ₹4000000
|
||||
first year then ₹4000000/yr
|
||||
@@ -1,5 +0,0 @@
|
||||
subtotal: ₹4000000
|
||||
tax: ₹700000
|
||||
discount: -₹700000
|
||||
total: ₹4000000
|
||||
first year then ₹4000000/yr
|
||||
@@ -1,5 +0,0 @@
|
||||
subtotal: ₹4000000
|
||||
tax: ₹700000
|
||||
discount: ₹0000
|
||||
total: ₹4000000/yr
|
||||
forever
|
||||
@@ -1,5 +0,0 @@
|
||||
subtotal: ₹4000000
|
||||
tax: ₹700000
|
||||
discount: ₹0000
|
||||
total: ₹4000000/yr
|
||||
forever
|
||||
@@ -1,5 +0,0 @@
|
||||
subtotal: ₹4000000
|
||||
tax: ₹700000
|
||||
discount: ₹0000
|
||||
total: ₹4000000/yr
|
||||
forever
|
||||
@@ -1,5 +0,0 @@
|
||||
subtotal: $40000
|
||||
tax: $0000
|
||||
discount: -$7000
|
||||
total: $40000
|
||||
first year then $40000/yr
|
||||
@@ -1,5 +0,0 @@
|
||||
subtotal: $40000
|
||||
tax: $0000
|
||||
discount: -$7000
|
||||
total: $40000
|
||||
first year then $40000/yr
|
||||
@@ -1,5 +0,0 @@
|
||||
subtotal: $40000
|
||||
tax: $0000
|
||||
discount: -$7000
|
||||
total: $40000
|
||||
first year then $40000/yr
|
||||
@@ -1,5 +0,0 @@
|
||||
subtotal: $40000
|
||||
tax: $0000
|
||||
discount: $0000
|
||||
total: $40000/yr
|
||||
forever
|
||||
@@ -1,5 +0,0 @@
|
||||
subtotal: $40000
|
||||
tax: $0000
|
||||
discount: $0000
|
||||
total: $40000/yr
|
||||
forever
|
||||
@@ -1,5 +0,0 @@
|
||||
subtotal: $40000
|
||||
tax: $0000
|
||||
discount: $0000
|
||||
total: $40000/yr
|
||||
forever
|
||||
180
apps/web/__e2e__/checkout.test.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2022 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { test, Page, expect } from "@playwright/test";
|
||||
import { USER } from "./utils";
|
||||
import { AppModel } from "./models/app.model";
|
||||
import { PriceItem } from "./models/types";
|
||||
|
||||
test.setTimeout(45 * 1000);
|
||||
test.describe.configure({ mode: "serial" });
|
||||
|
||||
let page: Page;
|
||||
let app: AppModel;
|
||||
|
||||
test.beforeAll(async ({ browser }) => {
|
||||
const { email, key, password } = USER.CURRENT;
|
||||
if (!email || !password) throw new Error("Failed to load user credentials.");
|
||||
|
||||
// Create page yourself and sign in.
|
||||
page = await browser.newPage();
|
||||
app = new AppModel(page);
|
||||
await app.auth.goto();
|
||||
await app.auth.login({ email, key, password });
|
||||
});
|
||||
|
||||
test.afterAll(async () => {
|
||||
await page.close();
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
await app.goto();
|
||||
});
|
||||
|
||||
function roundOffPrices(prices: PriceItem[]) {
|
||||
return prices
|
||||
.map((p) => {
|
||||
const price = p.value.replace(/(\d+).(\d+)/gm, (_, whole, decimal) => {
|
||||
const decimalDigits = "0".repeat(decimal.length);
|
||||
const wholeDigits = "0".repeat(whole.length - 1);
|
||||
return `${whole[0]}${wholeDigits}.${decimalDigits}`;
|
||||
});
|
||||
return `${p.label}: ${price}`;
|
||||
})
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
test("change plans", async () => {
|
||||
await app.checkout.goto();
|
||||
const plans = await app.checkout.getPlans();
|
||||
|
||||
const titles: string[] = [];
|
||||
for (const plan of plans) {
|
||||
const pricing = await plan.open();
|
||||
const title = await pricing.getTitle();
|
||||
if (title) titles.push(title);
|
||||
await pricing.changePlan();
|
||||
}
|
||||
|
||||
expect(titles).toHaveLength(2);
|
||||
expect(titles.join("").length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("confirm plan prices", async () => {
|
||||
await app.checkout.goto();
|
||||
const plans = await app.checkout.getPlans();
|
||||
|
||||
const planPrices: Record<string, string> = {};
|
||||
for (const plan of plans) {
|
||||
const pricing = await plan.open();
|
||||
const title = await pricing.getTitle();
|
||||
if (!title) continue;
|
||||
|
||||
planPrices[title.toLowerCase()] = roundOffPrices(await pricing.getPrices());
|
||||
await pricing.changePlan();
|
||||
}
|
||||
|
||||
for (const key in planPrices) {
|
||||
expect(planPrices[key]).toMatchSnapshot(`checkout-${key}-prices.txt`);
|
||||
}
|
||||
});
|
||||
|
||||
test("changing locale should show localized prices", async () => {
|
||||
await app.checkout.goto();
|
||||
const plans = await app.checkout.getPlans();
|
||||
|
||||
const planPrices: Record<string, string> = {};
|
||||
for (const plan of plans) {
|
||||
const pricing = await plan.open();
|
||||
const title = await pricing.getTitle();
|
||||
if (!title) continue;
|
||||
await pricing.changeCountry("IN", 110001);
|
||||
|
||||
planPrices[title.toLowerCase()] = roundOffPrices(await pricing.getPrices());
|
||||
await pricing.changePlan();
|
||||
}
|
||||
|
||||
for (const key in planPrices) {
|
||||
expect(planPrices[key]).toMatchSnapshot(`checkout-${key}-IN-prices.txt`);
|
||||
}
|
||||
});
|
||||
|
||||
test("applying coupon should change discount & total price", async () => {
|
||||
await app.checkout.goto();
|
||||
const plans = await app.checkout.getPlans();
|
||||
|
||||
const planPrices: Record<string, string> = {};
|
||||
for (const plan of plans) {
|
||||
const pricing = await plan.open();
|
||||
const title = await pricing.getTitle();
|
||||
if (!title) continue;
|
||||
await pricing.applyCoupon("INTRO50");
|
||||
|
||||
planPrices[title.toLowerCase()] = roundOffPrices(await pricing.getPrices());
|
||||
await pricing.changePlan();
|
||||
}
|
||||
|
||||
for (const key in planPrices) {
|
||||
expect(planPrices[key]).toMatchSnapshot(
|
||||
`checkout-${key}-discounted-prices.txt`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("apply coupon through url", async () => {
|
||||
const planPrices: Record<string, string> = {};
|
||||
for (const plan of ["monthly", "yearly"] as const) {
|
||||
await app.checkout.goto(plan, "INTRO50");
|
||||
const pricing = await app.checkout.getPricing();
|
||||
await pricing.waitForCoupon();
|
||||
|
||||
planPrices[plan] = roundOffPrices(await pricing.getPrices());
|
||||
|
||||
await app.goto();
|
||||
}
|
||||
|
||||
for (const key in planPrices) {
|
||||
expect(planPrices[key]).toMatchSnapshot(
|
||||
`checkout-${key}-discounted-prices.txt`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("apply coupon after changing country", async () => {
|
||||
await app.checkout.goto();
|
||||
const plans = await app.checkout.getPlans();
|
||||
|
||||
const planPrices: Record<string, string> = {};
|
||||
for (const plan of plans) {
|
||||
const pricing = await plan.open();
|
||||
const title = await pricing.getTitle();
|
||||
if (!title) continue;
|
||||
await pricing.changeCountry("IN", 110001);
|
||||
await pricing.applyCoupon("INTRO50");
|
||||
|
||||
planPrices[title.toLowerCase()] = roundOffPrices(await pricing.getPrices());
|
||||
await pricing.changePlan();
|
||||
}
|
||||
|
||||
for (const key in planPrices) {
|
||||
expect(planPrices[key]).toMatchSnapshot(
|
||||
`checkout-${key}-IN-discounted-prices.txt`
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
Subtotal: ₹300.00
|
||||
Sales tax: ₹60.00
|
||||
Discount: -₹200.00
|
||||
Total: ₹200.00
|
||||
@@ -0,0 +1,4 @@
|
||||
Subtotal: ₹300.00
|
||||
Sales tax: ₹60.00
|
||||
Discount: ₹0.00
|
||||
Total: ₹400.00/mo
|
||||
@@ -0,0 +1,4 @@
|
||||
Subtotal: $4.00
|
||||
Sales tax: $0.00
|
||||
Discount: -$2.00
|
||||
Total: $2.00
|
||||
@@ -0,0 +1,4 @@
|
||||
Subtotal: $4.00
|
||||
Sales tax: $0.00
|
||||
Discount: $0.00
|
||||
Total: $4.00/mo
|
||||
@@ -0,0 +1,4 @@
|
||||
Subtotal: $4.00
|
||||
Sales tax: $0.00
|
||||
Discount: $0.00
|
||||
Total: $4.00/mo
|
||||
@@ -0,0 +1,4 @@
|
||||
Subtotal: $4.00
|
||||
Sales tax: $0.00
|
||||
Discount: $0.00
|
||||
Total: $4.00/mo
|
||||
@@ -0,0 +1,4 @@
|
||||
Subtotal: ₹4000.00
|
||||
Sales tax: ₹700.00
|
||||
Discount: -₹2000.00
|
||||
Total: ₹2000.00
|
||||
@@ -0,0 +1,4 @@
|
||||
Subtotal: ₹4000.00
|
||||
Sales tax: ₹700.00
|
||||
Discount: ₹0.00
|
||||
Total: ₹4000.00/yr
|
||||
@@ -0,0 +1,4 @@
|
||||
Subtotal: $40.00
|
||||
Sales tax: $0.00
|
||||
Discount: -$20.00
|
||||
Total: $20.00
|
||||
@@ -0,0 +1,4 @@
|
||||
Subtotal: $40.00
|
||||
Sales tax: $0.00
|
||||
Discount: $0.00
|
||||
Total: $40.00/yr
|
||||
@@ -0,0 +1,4 @@
|
||||
Subtotal: $40.00
|
||||
Sales tax: $0.00
|
||||
Discount: $0.00
|
||||
Total: $40.00/yr
|
||||
@@ -0,0 +1,4 @@
|
||||
Subtotal: $40.00
|
||||
Sales tax: $0.00
|
||||
Discount: $0.00
|
||||
Total: $40.00/yr
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2022 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const { test } = require("@playwright/test");
|
||||
const { getTestId } = require("./utils");
|
||||
const { useContextMenu, clickMenuItem } = require("./utils/actions");
|
||||
const { createNoteAndCheckPresence } = require("./utils/conditions");
|
||||
const Menu = require("./utils/menuitemidbuilder");
|
||||
|
||||
// test.skip(
|
||||
// "TODO: make sure to navigate to home if there are 0 notes in a color"
|
||||
// );
|
||||
|
||||
/**
|
||||
* @type {import("@playwright/test").Page}
|
||||
*/
|
||||
var page = null;
|
||||
global.page = null;
|
||||
|
||||
test.beforeEach(async ({ page: _page, baseURL }) => {
|
||||
global.page = _page;
|
||||
page = _page;
|
||||
await page.goto(baseURL);
|
||||
await page.waitForSelector(getTestId("routeHeader"));
|
||||
});
|
||||
|
||||
test("delete the last note of a color", async ({ page }) => {
|
||||
const noteSelector = await createNoteAndCheckPresence();
|
||||
|
||||
await useContextMenu(noteSelector, async () => {
|
||||
await clickMenuItem("colors");
|
||||
await clickMenuItem("red");
|
||||
});
|
||||
|
||||
const navItem = new Menu("navitem").item("red").build();
|
||||
await page.waitForSelector(navItem);
|
||||
|
||||
await useContextMenu(noteSelector, async () => {
|
||||
await clickMenuItem("movetotrash");
|
||||
});
|
||||
|
||||
await page.click(new Menu("navitem").item("trash").build());
|
||||
});
|
||||
48
apps/web/__e2e__/colors.test.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2022 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { AppModel } from "./models/app.model";
|
||||
import { NOTE } from "./utils";
|
||||
|
||||
test("delete the last note of a color", async ({ page }) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const notes = await app.goToNotes();
|
||||
const note = await notes.createNote(NOTE);
|
||||
await note?.contextMenu.color("red");
|
||||
await app.navigation.findItem("red");
|
||||
|
||||
await note?.contextMenu.moveToTrash();
|
||||
|
||||
await app.goToTrash();
|
||||
expect(await app.getRouteHeader()).toBe("Trash");
|
||||
});
|
||||
|
||||
test("rename color", async ({ page }) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const notes = await app.goToNotes();
|
||||
const note = await notes.createNote(NOTE);
|
||||
await note?.contextMenu.color("red");
|
||||
const colorItem = await app.navigation.findItem("red");
|
||||
|
||||
await colorItem?.renameColor("priority-33");
|
||||
|
||||
expect(await app.navigation.findItem("priority-33"));
|
||||
});
|
||||
@@ -1,349 +0,0 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2022 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const { test, expect } = require("@playwright/test");
|
||||
const {
|
||||
createNote,
|
||||
NOTE,
|
||||
getTestId,
|
||||
getEditorTitle,
|
||||
getEditorContent,
|
||||
getEditorContentAsHTML,
|
||||
editNote
|
||||
} = require("./utils");
|
||||
const {
|
||||
checkNotePresence,
|
||||
createNoteAndCheckPresence
|
||||
} = require("./utils/conditions");
|
||||
/**
|
||||
* @type {import("@playwright/test").Page}
|
||||
*/
|
||||
var page = null;
|
||||
global.page = null;
|
||||
test.beforeEach(async ({ page: _page, baseURL }) => {
|
||||
global.page = _page;
|
||||
page = _page;
|
||||
await page.goto(baseURL);
|
||||
await page.waitForSelector(getTestId("routeHeader"));
|
||||
});
|
||||
|
||||
test("focus mode", async () => {
|
||||
await createNote(NOTE, "notes");
|
||||
|
||||
await page.click(getTestId("focus-mode"));
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
expect(
|
||||
await page.screenshot({ fullPage: true, quality: 100, type: "jpeg" })
|
||||
).toMatchSnapshot("focus-mode.jpg", { threshold: 99 });
|
||||
});
|
||||
|
||||
test("dark mode in focus mode", async () => {
|
||||
await createNote(NOTE, "notes");
|
||||
|
||||
await page.click(getTestId("focus-mode"));
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.click(getTestId("dark-mode"));
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
expect(
|
||||
await page.screenshot({ fullPage: true, quality: 100, type: "jpeg" })
|
||||
).toMatchSnapshot("dark-focus-mode.jpg", { threshold: 99 });
|
||||
|
||||
await page.click(getTestId("dark-mode"));
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
expect(
|
||||
await page.screenshot({ fullPage: true, quality: 100, type: "jpeg" })
|
||||
).toMatchSnapshot("light-focus-mode.jpg", { threshold: 99 });
|
||||
});
|
||||
|
||||
test("full screen in focus mode", async () => {
|
||||
await createNote(NOTE, "notes");
|
||||
|
||||
await page.click(getTestId("focus-mode"));
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.click(getTestId("enter-fullscreen"));
|
||||
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
await page.click(getTestId("exit-fullscreen"));
|
||||
});
|
||||
|
||||
test("normal mode from focus mode", async () => {
|
||||
await createNote(NOTE, "notes");
|
||||
|
||||
await page.click(getTestId("focus-mode"));
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.click(getTestId("normal-mode"));
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
expect(
|
||||
await page.screenshot({ fullPage: true, quality: 100, type: "jpeg" })
|
||||
).toMatchSnapshot("normal-mode-from-focus-mode.jpg", { threshold: 99 });
|
||||
});
|
||||
|
||||
test("creating a new note should clear the editor contents & title", async () => {
|
||||
await createNoteAndCheckPresence();
|
||||
|
||||
await page.click(getTestId("notes-action-button"));
|
||||
|
||||
expect(await getEditorTitle()).toBe("");
|
||||
|
||||
expect(await getEditorContent()).toBe("");
|
||||
});
|
||||
|
||||
test("creating a new note should clear the word count", async () => {
|
||||
const selector = await createNoteAndCheckPresence();
|
||||
|
||||
await page.click(getTestId("notes-action-button"));
|
||||
|
||||
await page.click(selector);
|
||||
|
||||
await createNote({ title: "Hello World" }, "notes");
|
||||
|
||||
await expect(page.innerText(getTestId("editor-word-count"))).resolves.toBe(
|
||||
"0 words"
|
||||
);
|
||||
});
|
||||
|
||||
test("creating a new title-only note should add it to the list", async () => {
|
||||
const selector = await createNoteAndCheckPresence();
|
||||
|
||||
await page.click(getTestId("notes-action-button"));
|
||||
|
||||
await page.click(selector);
|
||||
|
||||
await createNoteAndCheckPresence({ title: "Hello World" });
|
||||
});
|
||||
|
||||
test.skip("format changes should get saved", async () => {
|
||||
const selector = await createNoteAndCheckPresence();
|
||||
|
||||
await page.click(getTestId("notes-action-button"));
|
||||
|
||||
await page.click(selector);
|
||||
|
||||
await page.waitForSelector(".mce-content-body");
|
||||
|
||||
await page.keyboard.press("Shift+End");
|
||||
|
||||
await page.click(`#editorToolbar button[title="Bold"]`);
|
||||
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
await page.click(getTestId("notes-action-button"));
|
||||
|
||||
await page.click(selector);
|
||||
|
||||
const content = await getEditorContentAsHTML();
|
||||
|
||||
expect(content).toMatchSnapshot(`format-changes-should-get-saved.txt`);
|
||||
});
|
||||
|
||||
test("opening an empty titled note should empty out editor contents", async () => {
|
||||
await createNoteAndCheckPresence();
|
||||
|
||||
const onlyTitle = await createNoteAndCheckPresence({
|
||||
title: "Only a title"
|
||||
});
|
||||
|
||||
await page.click(getTestId("notes-action-button"));
|
||||
|
||||
await page.reload();
|
||||
|
||||
const fullNote = await checkNotePresence("home", 1, NOTE);
|
||||
|
||||
await page.click(fullNote);
|
||||
|
||||
await expect(getEditorContent()).resolves.toBe(NOTE.content);
|
||||
|
||||
await expect(getEditorTitle()).resolves.toBe(NOTE.title);
|
||||
|
||||
await page.click(onlyTitle);
|
||||
|
||||
await expect(getEditorTitle()).resolves.toBe("Only a title");
|
||||
|
||||
await expect(getEditorContent()).resolves.toBe("");
|
||||
});
|
||||
|
||||
test("focus should not jump to editor while typing in title input", async () => {
|
||||
await page.click(getTestId("notes-action-button"));
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
|
||||
await page.type(getTestId("editor-title"), "Hello", { delay: 200 });
|
||||
|
||||
await expect(getEditorTitle()).resolves.toBe("Hello");
|
||||
|
||||
await expect(getEditorContent()).resolves.toBe("");
|
||||
});
|
||||
|
||||
test("select all & backspace should clear all content in editor", async () => {
|
||||
const selector = await createNoteAndCheckPresence();
|
||||
|
||||
await page.focus(".ProseMirror");
|
||||
|
||||
await page.keyboard.press("Home");
|
||||
|
||||
await page.keyboard.press("Shift+End");
|
||||
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
await page.keyboard.press("Backspace");
|
||||
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
await page.click(getTestId("notes-action-button"));
|
||||
|
||||
await page.click(selector);
|
||||
|
||||
await page.waitForSelector(".ProseMirror");
|
||||
|
||||
await expect(getEditorContent()).resolves.toBe("");
|
||||
});
|
||||
|
||||
test.skip("last line doesn't get saved if it's font is different", async () => {
|
||||
const selector = await createNoteAndCheckPresence();
|
||||
|
||||
await page.keyboard.press("Enter");
|
||||
|
||||
await page.click(`#editorToolbar button[title="Fonts"]`);
|
||||
|
||||
await page.click(`div[title="Serif"]`);
|
||||
|
||||
await page.type(".ProseMirror", "I am another line in Serif font.");
|
||||
|
||||
await page.waitForTimeout(200);
|
||||
|
||||
await page.click(getTestId("notes-action-button"));
|
||||
|
||||
await page.click(selector);
|
||||
|
||||
const content = await getEditorContentAsHTML();
|
||||
|
||||
expect(content).toMatchSnapshot(`last-line-with-different-font.txt`);
|
||||
});
|
||||
|
||||
test("editing a note and switching immediately to another note and making an edit shouldn't overlap both notes", async ({
|
||||
page
|
||||
}) => {
|
||||
await createNoteAndCheckPresence({
|
||||
title: "Test note 1",
|
||||
content: "53ad8e4e40ebebd0f400498d"
|
||||
});
|
||||
|
||||
await createNoteAndCheckPresence({
|
||||
title: "Test note 2",
|
||||
content: "f054d19e9a2f46eff7b9bb25"
|
||||
});
|
||||
|
||||
const selector1 = `[data-item-index="1"] div`;
|
||||
const selector2 = `[data-item-index="2"] div`;
|
||||
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
await page.click(selector2);
|
||||
|
||||
await editNote(null, `Test note 1 (${i}) `);
|
||||
|
||||
await page.click(selector2);
|
||||
|
||||
await editNote(null, `Test note 2 (${i})`);
|
||||
}
|
||||
|
||||
await page.click(selector1);
|
||||
expect(await getEditorContent()).toMatchSnapshot(
|
||||
`fast-switch-and-edit-note-2.txt`
|
||||
);
|
||||
|
||||
await page.click(selector2);
|
||||
expect(await getEditorContent()).toMatchSnapshot(
|
||||
`fast-switch-and-edit-note-1.txt`
|
||||
);
|
||||
});
|
||||
|
||||
test("editing a note and switching immediately to another note and editing the title shouldn't overlap both notes", async ({
|
||||
page
|
||||
}, testInfo) => {
|
||||
testInfo.setTimeout(0);
|
||||
await createNoteAndCheckPresence({
|
||||
title: "Test note 1",
|
||||
content: "53ad8e4e40ebebd0f400498d"
|
||||
});
|
||||
|
||||
await createNoteAndCheckPresence({
|
||||
title: "Test note 2",
|
||||
content: "f054d19e9a2f46eff7b9bb25"
|
||||
});
|
||||
|
||||
const selector1 = `[data-item-index="1"] div`;
|
||||
const selector2 = `[data-item-index="2"] div`;
|
||||
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
await page.click(selector2);
|
||||
|
||||
await editNote(`Test note 1 (${i})`, null, true);
|
||||
|
||||
await page.waitForTimeout(100);
|
||||
|
||||
await page.click(selector2);
|
||||
|
||||
await editNote(`Test note 2 (${i})`, null, true);
|
||||
|
||||
await page.waitForTimeout(100);
|
||||
}
|
||||
|
||||
await page.click(selector1);
|
||||
expect(await getEditorTitle()).toMatchSnapshot(
|
||||
`fast-switch-and-edit-note-title-2.txt`
|
||||
);
|
||||
|
||||
await page.click(selector2);
|
||||
expect(await getEditorTitle()).toMatchSnapshot(
|
||||
`fast-switch-and-edit-note-title-1.txt`
|
||||
);
|
||||
});
|
||||
|
||||
test("editing a note and toggling read-only mode should show updated content", async () => {
|
||||
const selector = await createNoteAndCheckPresence();
|
||||
|
||||
await page.click(getTestId("notes-action-button"));
|
||||
|
||||
await page.click(selector);
|
||||
|
||||
await editNote(null, `An edit I made`, false);
|
||||
|
||||
await page.click(getTestId("properties"));
|
||||
|
||||
await page.click(getTestId("properties-readonly"));
|
||||
|
||||
await page.click(getTestId("properties-close"));
|
||||
|
||||
expect(await getEditorContent()).toMatchSnapshot(`readonly-edited-note.txt`);
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
Test note 1 (9)
|
||||
@@ -1 +0,0 @@
|
||||
Test note 1 (9)
|
||||
@@ -1 +0,0 @@
|
||||
Test note 1 (9)
|
||||
@@ -1 +0,0 @@
|
||||
Test note 2 (9)
|
||||
@@ -1 +0,0 @@
|
||||
Test note 2 (9)
|
||||
@@ -1 +0,0 @@
|
||||
Test note 2 (9)
|
||||
@@ -1 +0,0 @@
|
||||
<p><strong>This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1</strong></p>
|
||||
@@ -1 +0,0 @@
|
||||
<p><strong>This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1</strong><br></p>
|
||||
@@ -1 +0,0 @@
|
||||
<p><strong>This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1</strong></p>
|
||||
@@ -1 +0,0 @@
|
||||
<p>This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1<span id="_mce_caret" data-mce-bogus="1"><span style="font-family: serif;">I am another line in Serif font.</span></span></p>
|
||||
@@ -1 +0,0 @@
|
||||
<p>This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1<span id="_mce_caret" data-mce-bogus="1"><span style="font-family: serif;">I am another line in Serif font.</span></span><br></p>
|
||||
@@ -1 +0,0 @@
|
||||
<p>This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1<span id="_mce_caret" data-mce-bogus="1"><span style="font-family: serif;">I am another line in Serif font.</span></span></p>
|
||||
|
Before Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 179 KiB |
261
apps/web/__e2e__/editor.test.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2022 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { AppModel } from "./models/app.model";
|
||||
import { NOTE, TITLE_ONLY_NOTE } from "./utils";
|
||||
|
||||
test("focus mode", async ({ page }) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const notes = await app.goToNotes();
|
||||
await notes.createNote(NOTE);
|
||||
|
||||
await notes.editor.enterFocusMode();
|
||||
|
||||
expect(await notes.editor.isFocusMode()).toBeTruthy();
|
||||
expect(
|
||||
await page.screenshot({ fullPage: true, quality: 100, type: "jpeg" })
|
||||
).toMatchSnapshot("focus-mode.jpg", { maxDiffPixelRatio: 0.01 });
|
||||
});
|
||||
|
||||
test("switch theme in focus mode", async ({ page }) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const notes = await app.goToNotes();
|
||||
await notes.createNote(NOTE);
|
||||
await notes.editor.enterFocusMode();
|
||||
|
||||
await notes.editor.enterDarkMode();
|
||||
|
||||
expect(await notes.editor.isDarkMode()).toBeTruthy();
|
||||
expect(
|
||||
await page.screenshot({ fullPage: true, quality: 100, type: "jpeg" })
|
||||
).toMatchSnapshot("dark-focus-mode.jpg", { maxDiffPixelRatio: 0.01 });
|
||||
|
||||
await notes.editor.exitDarkMode();
|
||||
|
||||
expect(await notes.editor.isDarkMode()).toBeFalsy();
|
||||
expect(
|
||||
await page.screenshot({ fullPage: true, quality: 100, type: "jpeg" })
|
||||
).toMatchSnapshot("light-focus-mode.jpg", { maxDiffPixelRatio: 0.01 });
|
||||
});
|
||||
|
||||
test("full screen in focus mode", async ({ page }) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const notes = await app.goToNotes();
|
||||
await notes.createNote(NOTE);
|
||||
await notes.editor.enterFocusMode();
|
||||
|
||||
await notes.editor.enterFullscreen();
|
||||
expect(await notes.editor.isFullscreen()).toBeTruthy();
|
||||
|
||||
await notes.editor.exitFullscreen();
|
||||
expect(await notes.editor.isFullscreen()).toBeFalsy();
|
||||
});
|
||||
|
||||
test("normal mode from focus mode", async ({ page }) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const notes = await app.goToNotes();
|
||||
await notes.createNote(NOTE);
|
||||
await notes.editor.enterFocusMode();
|
||||
|
||||
await notes.editor.exitFocusMode();
|
||||
|
||||
expect(await notes.editor.isFocusMode()).toBeFalsy();
|
||||
expect(
|
||||
await page.screenshot({ fullPage: true, quality: 100, type: "jpeg" })
|
||||
).toMatchSnapshot("normal-mode-from-focus-mode.jpg", {
|
||||
maxDiffPixelRatio: 0.01
|
||||
});
|
||||
});
|
||||
|
||||
test("creating a new note should clear the editor contents & title", async ({
|
||||
page
|
||||
}) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const notes = await app.goToNotes();
|
||||
await notes.createNote(NOTE);
|
||||
|
||||
await notes.newNote();
|
||||
|
||||
expect(await notes.editor.isUnloaded()).toBeTruthy();
|
||||
expect(await notes.editor.getTitle()).toBe("");
|
||||
expect(await notes.editor.getContent("text")).toBe("");
|
||||
});
|
||||
|
||||
test("creating a new title-only note should add it to the list", async ({
|
||||
page
|
||||
}) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const notes = await app.goToNotes();
|
||||
|
||||
const note = await notes.createNote({ title: NOTE.title });
|
||||
|
||||
expect(note).toBeDefined();
|
||||
});
|
||||
|
||||
test("opening an empty titled note should empty out editor contents", async ({
|
||||
page
|
||||
}) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
let notes = await app.goToNotes();
|
||||
await notes.createNote(NOTE);
|
||||
await notes.createNote(TITLE_ONLY_NOTE);
|
||||
await notes.newNote();
|
||||
await app.goto();
|
||||
notes = await app.goToNotes();
|
||||
const fullNote = await notes.findNote(NOTE);
|
||||
await fullNote?.openNote();
|
||||
|
||||
const onlyTitle = await notes.findNote(TITLE_ONLY_NOTE);
|
||||
await onlyTitle?.openNote();
|
||||
|
||||
expect(await notes.editor.getContent("text")).toBe("");
|
||||
expect(await notes.editor.getTitle()).toBe(TITLE_ONLY_NOTE.title);
|
||||
});
|
||||
|
||||
test("focus should not jump to editor while typing in title input", async ({
|
||||
page
|
||||
}) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const notes = await app.goToNotes();
|
||||
await notes.newNote();
|
||||
|
||||
await notes.editor.typeTitle("Hello", 200);
|
||||
|
||||
expect(await notes.editor.getTitle()).toBe("Hello");
|
||||
expect(await notes.editor.getContent("text")).toBe("");
|
||||
});
|
||||
|
||||
test("select all & backspace should clear all content in editor", async ({
|
||||
page
|
||||
}) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const notes = await app.goToNotes();
|
||||
const note = await notes.createNote(NOTE);
|
||||
|
||||
await notes.editor.selectAll();
|
||||
await page.keyboard.press("Backspace");
|
||||
await notes.editor.waitForSaving();
|
||||
|
||||
await notes.newNote();
|
||||
await note?.openNote();
|
||||
expect(await notes.editor.getContent("text")).toBe("");
|
||||
});
|
||||
|
||||
test("editing a note and switching immediately to another note and making an edit shouldn't overlap both notes", async ({
|
||||
page
|
||||
}) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const notes = await app.goToNotes();
|
||||
const note1 = await notes.createNote({
|
||||
title: "Test note 1",
|
||||
content: "53ad8e4e40ebebd0f400498d"
|
||||
});
|
||||
const note2 = await notes.createNote({
|
||||
title: "Test note 2",
|
||||
content: "f054d19e9a2f46eff7b9bb25"
|
||||
});
|
||||
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
await note1?.openNote();
|
||||
await notes.editor.setContent(`Test note 1 (${i}) `);
|
||||
|
||||
await note2?.openNote();
|
||||
await notes.editor.setContent(`Test note 2 (${i})`);
|
||||
}
|
||||
|
||||
await notes.editor.waitForSaving();
|
||||
await notes.newNote();
|
||||
|
||||
await note2?.openNote();
|
||||
expect(await notes.editor.getContent("text")).toMatchSnapshot(
|
||||
`fast-switch-and-edit-note-2.txt`
|
||||
);
|
||||
|
||||
await note1?.openNote();
|
||||
expect(await notes.editor.getContent("text")).toMatchSnapshot(
|
||||
`fast-switch-and-edit-note-1.txt`
|
||||
);
|
||||
});
|
||||
|
||||
test("editing a note and switching immediately to another note and editing the title shouldn't overlap both notes", async ({
|
||||
page
|
||||
}) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const notes = await app.goToNotes();
|
||||
const note1 = await notes.createNote({
|
||||
title: "Test note 1",
|
||||
content: "53ad8e4e40ebebd0f400498d"
|
||||
});
|
||||
const note2 = await notes.createNote({
|
||||
title: "Test note 2",
|
||||
content: "f054d19e9a2f46eff7b9bb25"
|
||||
});
|
||||
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
await note1?.openNote();
|
||||
await notes.editor.typeTitle(`Test note 1 (${i}) `);
|
||||
|
||||
await note2?.openNote();
|
||||
await notes.editor.typeTitle(`Test note 2 (${i})`);
|
||||
}
|
||||
|
||||
await notes.editor.waitForSaving();
|
||||
await notes.newNote();
|
||||
|
||||
await note2?.openNote();
|
||||
expect(await notes.editor.getTitle()).toMatchSnapshot(
|
||||
`fast-switch-and-edit-note-title-2.txt`
|
||||
);
|
||||
|
||||
await note1?.openNote();
|
||||
expect(await notes.editor.getTitle()).toMatchSnapshot(
|
||||
`fast-switch-and-edit-note-title-1.txt`
|
||||
);
|
||||
});
|
||||
|
||||
test("editing a note and toggling read-only mode should show updated content", async ({
|
||||
page
|
||||
}) => {
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const notes = await app.goToNotes();
|
||||
const note = await notes.createNote(NOTE);
|
||||
await notes.newNote();
|
||||
await note?.openNote();
|
||||
|
||||
await notes.editor.setContent(`An edit I made`);
|
||||
await note?.properties.readonly();
|
||||
|
||||
expect(await note?.properties.isReadonly()).toBeTruthy();
|
||||
expect(await notes.editor.getContent("text")).toMatchSnapshot(
|
||||
`readonly-edited-note.txt`
|
||||
);
|
||||
});
|
||||
|
Before Width: | Height: | Size: 43 KiB After Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 88 KiB |
@@ -0,0 +1 @@
|
||||
Test note 1Test note 1 (0) Test note 1 (1) Test note 1 (2) Test note 1 (3) Test note 1 (4) Test note 1 (5) Test note 1 (6) Test note 1 (7) Test note 1 (8) Test note 1 (9)
|
||||
@@ -0,0 +1 @@
|
||||
Test note 1Test note 1 (0) Test note 1 (1) Test note 1 (2) Test note 1 (3) Test note 1 (4) Test note 1 (5) Test note 1 (6) Test note 1 (7) Test note 1 (8) Test note 1 (9)
|
||||
@@ -0,0 +1 @@
|
||||
Test note 1Test note 1 (0) Test note 1 (1) Test note 1 (2) Test note 1 (3) Test note 1 (4) Test note 1 (5) Test note 1 (6) Test note 1 (7) Test note 1 (8) Test note 1 (9)
|
||||
@@ -0,0 +1 @@
|
||||
Test note 2Test note 2 (0)Test note 2 (1)Test note 2 (2)Test note 2 (3)Test note 2 (4)Test note 2 (5)Test note 2 (6)Test note 2 (7)Test note 2 (8)Test note 2 (9)
|
||||
@@ -0,0 +1 @@
|
||||
Test note 2Test note 2 (0)Test note 2 (1)Test note 2 (2)Test note 2 (3)Test note 2 (4)Test note 2 (5)Test note 2 (6)Test note 2 (7)Test note 2 (8)Test note 2 (9)
|
||||
@@ -0,0 +1 @@
|
||||
Test note 2Test note 2 (0)Test note 2 (1)Test note 2 (2)Test note 2 (3)Test note 2 (4)Test note 2 (5)Test note 2 (6)Test note 2 (7)Test note 2 (8)Test note 2 (9)
|
||||
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 72 KiB |
|
After Width: | Height: | Size: 171 KiB |
@@ -17,48 +17,48 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const { test, expect } = require("@playwright/test");
|
||||
const { getTestId, isTestAll, loginUser } = require("./utils");
|
||||
const path = require("path");
|
||||
// const { test, expect } = require("@playwright/test");
|
||||
// const { getTestId, isTestAll, loginUser } = require("./utils");
|
||||
// const path = require("path");
|
||||
|
||||
/**
|
||||
* @type {import("@playwright/test").Page}
|
||||
*/
|
||||
var page = null;
|
||||
global.page = null;
|
||||
test.beforeEach(async ({ page: _page, baseURL }) => {
|
||||
global.page = _page;
|
||||
page = _page;
|
||||
await page.goto(baseURL);
|
||||
await page.waitForSelector(getTestId("routeHeader"));
|
||||
});
|
||||
// /**
|
||||
// * @type {import("@playwright/test").Page}
|
||||
// */
|
||||
// var page = null;
|
||||
// global.page = null;
|
||||
// test.beforeEach(async ({ page: _page, baseURL }) => {
|
||||
// global.page = _page;
|
||||
// page = _page;
|
||||
// await page.goto(baseURL);
|
||||
// await page.waitForSelector(getTestId("routeHeader"));
|
||||
// });
|
||||
|
||||
if (!isTestAll()) test.skip();
|
||||
// if (!isTestAll()) test.skip();
|
||||
|
||||
test("login user & import notes", async () => {
|
||||
await loginUser();
|
||||
// test("login user & import notes", async () => {
|
||||
// await loginUser();
|
||||
|
||||
await page.click(getTestId("navitem-settings"));
|
||||
// await page.click(getTestId("navitem-settings"));
|
||||
|
||||
await page.click(getTestId("settings-importer"));
|
||||
// await page.click(getTestId("settings-importer"));
|
||||
|
||||
await page.click(getTestId("settings-importer-import"));
|
||||
// await page.click(getTestId("settings-importer-import"));
|
||||
|
||||
const [fileChooser] = await Promise.all([
|
||||
page.waitForEvent("filechooser"),
|
||||
page.click(getTestId("import-dialog-select-files"))
|
||||
]);
|
||||
// const [fileChooser] = await Promise.all([
|
||||
// page.waitForEvent("filechooser"),
|
||||
// page.click(getTestId("import-dialog-select-files"))
|
||||
// ]);
|
||||
|
||||
await fileChooser.setFiles(path.join(__dirname, "data", "importer-data.zip"));
|
||||
// await fileChooser.setFiles(path.join(__dirname, "data", "importer-data.zip"));
|
||||
|
||||
await page.click(getTestId("importer-dialog-notes"));
|
||||
// await page.click(getTestId("importer-dialog-notes"));
|
||||
|
||||
let titles = [];
|
||||
for (let i = 0; i < 6; ++i) {
|
||||
const noteId = getTestId(`note-${i}-title`);
|
||||
const text = await page.innerText(noteId);
|
||||
titles.push(text);
|
||||
}
|
||||
// let titles = [];
|
||||
// for (let i = 0; i < 6; ++i) {
|
||||
// const noteId = getTestId(`note-${i}-title`);
|
||||
// const text = await page.innerText(noteId);
|
||||
// titles.push(text);
|
||||
// }
|
||||
|
||||
expect(titles.join("\n")).toMatchSnapshot("importer-notes-titles.txt");
|
||||
});
|
||||
// expect(titles.join("\n")).toMatchSnapshot("importer-notes-titles.txt");
|
||||
// });
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
2021-05-21T22_31_28.334+05_00
|
||||
2021-11-29T10_12_57.757+05_00
|
||||
2021-11-29T10_11_39.427+05_00
|
||||
2021-11-29T10_08_04.344+05_00
|
||||
2021-11-29T10_12_25.187+05_00
|
||||
Some really amazing note
|
||||
@@ -1,6 +0,0 @@
|
||||
2021-05-21T22_31_28.334+05_00
|
||||
2021-11-29T10_12_57.757+05_00
|
||||
2021-11-29T10_11_39.427+05_00
|
||||
2021-11-29T10_08_04.344+05_00
|
||||
2021-11-29T10_12_25.187+05_00
|
||||
Some really amazing note
|
||||
@@ -1,11 +0,0 @@
|
||||
2021-05-21T22_31_28.334+05_00
|
||||
|
||||
2021-11-29T10_12_57.757+05_00
|
||||
|
||||
2021-11-29T10_11_39.427+05_00
|
||||
|
||||
2021-11-29T10_08_04.344+05_00
|
||||
|
||||
2021-11-29T10_12_25.187+05_00
|
||||
|
||||
Some really amazing note
|
||||
106
apps/web/__e2e__/models/app.model.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2022 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Page } from "@playwright/test";
|
||||
import { getTestId } from "../utils";
|
||||
import { AuthModel } from "./auth.model";
|
||||
import { CheckoutModel } from "./checkout.model";
|
||||
import { ItemsViewModel } from "./items-view.model";
|
||||
import { NavigationMenuModel } from "./navigation-menu.model";
|
||||
import { NotebooksViewModel } from "./notebooks-view.model";
|
||||
import { NotesViewModel } from "./notes-view.model";
|
||||
import { SettingsViewModel } from "./settings-view.model";
|
||||
import { ToastsModel } from "./toasts.model";
|
||||
import { TrashViewModel } from "./trash-view.model";
|
||||
|
||||
export class AppModel {
|
||||
private readonly page: Page;
|
||||
readonly toasts: ToastsModel;
|
||||
readonly navigation: NavigationMenuModel;
|
||||
readonly auth: AuthModel;
|
||||
readonly checkout: CheckoutModel;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.toasts = new ToastsModel(page);
|
||||
this.navigation = new NavigationMenuModel(page);
|
||||
this.auth = new AuthModel(page);
|
||||
this.checkout = new CheckoutModel(page);
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto("/");
|
||||
await this.getRouteHeader();
|
||||
}
|
||||
|
||||
async goToNotes() {
|
||||
await this.navigateTo("Notes");
|
||||
return new NotesViewModel(this.page, "home");
|
||||
}
|
||||
|
||||
async goToNotebooks() {
|
||||
await this.navigateTo("Notebooks");
|
||||
return new NotebooksViewModel(this.page);
|
||||
}
|
||||
|
||||
async goToFavorites() {
|
||||
await this.navigateTo("Favorites");
|
||||
return new NotesViewModel(this.page, "notes");
|
||||
}
|
||||
|
||||
async goToTags() {
|
||||
await this.navigateTo("Tags");
|
||||
return new ItemsViewModel(this.page, "tags");
|
||||
}
|
||||
|
||||
async goToColor(color: string) {
|
||||
await this.navigateTo(color);
|
||||
return new NotesViewModel(this.page, "notes");
|
||||
}
|
||||
|
||||
async goToTrash() {
|
||||
await this.navigateTo("Trash");
|
||||
return new TrashViewModel(this.page);
|
||||
}
|
||||
|
||||
async goToSettings() {
|
||||
await this.navigateTo("Settings");
|
||||
return new SettingsViewModel(this.page);
|
||||
}
|
||||
|
||||
private async navigateTo(title: string) {
|
||||
if ((await this.getRouteHeader()) === title) return;
|
||||
const item = await this.navigation.findItem(title);
|
||||
await item?.click();
|
||||
await this.page.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
getRouteHeader() {
|
||||
return this.page.locator(getTestId("routeHeader")).inputValue();
|
||||
}
|
||||
|
||||
async isSynced() {
|
||||
return (
|
||||
(await this.page
|
||||
.locator(getTestId("sync-status-completed"))
|
||||
.isVisible()) ||
|
||||
(await this.page.locator(getTestId("sync-status-synced")).isVisible())
|
||||
);
|
||||
}
|
||||
}
|
||||
58
apps/web/__e2e__/models/auth.model.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2022 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Locator, Page } from "@playwright/test";
|
||||
import { getTestId } from "../utils";
|
||||
|
||||
type User = {
|
||||
email: string;
|
||||
password: string;
|
||||
key?: string;
|
||||
};
|
||||
|
||||
export class AuthModel {
|
||||
private readonly page: Page;
|
||||
private readonly emailInput: Locator;
|
||||
private readonly passwordInput: Locator;
|
||||
private readonly submitButton: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.emailInput = page.locator(getTestId("email"));
|
||||
this.passwordInput = page.locator(getTestId("password"));
|
||||
this.submitButton = page.locator(getTestId("submitButton"));
|
||||
}
|
||||
|
||||
async goto() {
|
||||
await this.page.goto("/login");
|
||||
}
|
||||
|
||||
async login(user: Partial<User>) {
|
||||
if (!user.email && !user.password) return;
|
||||
|
||||
if (user.email) await this.emailInput.fill(user.email);
|
||||
if (user.password) await this.passwordInput.fill(user.password);
|
||||
|
||||
await this.submitButton.click();
|
||||
|
||||
await this.page
|
||||
.locator(getTestId("sync-status-completed"))
|
||||
.waitFor({ state: "visible" });
|
||||
}
|
||||
}
|
||||
57
apps/web/__e2e__/models/base-item.model.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2022 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Locator, Page } from "@playwright/test";
|
||||
import { getTestId } from "../utils";
|
||||
|
||||
export class BaseItemModel {
|
||||
protected readonly page: Page;
|
||||
private readonly titleText: Locator;
|
||||
readonly descriptionText: Locator;
|
||||
|
||||
constructor(protected readonly locator: Locator) {
|
||||
this.page = locator.page();
|
||||
this.titleText = this.locator.locator(getTestId(`title`));
|
||||
this.descriptionText = this.locator.locator(getTestId(`description`));
|
||||
}
|
||||
|
||||
async getId() {
|
||||
return await this.locator.getAttribute("id");
|
||||
}
|
||||
|
||||
async getTitle() {
|
||||
if (await this.titleText.isVisible())
|
||||
return (await this.titleText.textContent()) || "";
|
||||
return "";
|
||||
}
|
||||
|
||||
async getDescription() {
|
||||
if (await this.descriptionText.isVisible())
|
||||
return (await this.descriptionText.textContent()) || "";
|
||||
return "";
|
||||
}
|
||||
|
||||
isPresent() {
|
||||
return this.locator.isVisible();
|
||||
}
|
||||
|
||||
waitFor(state: "attached" | "detached" | "visible" | "hidden") {
|
||||
return this.locator.waitFor({ state });
|
||||
}
|
||||
}
|
||||
61
apps/web/__e2e__/models/base-view.model.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2022 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Locator, Page } from "@playwright/test";
|
||||
import { getTestId } from "../utils";
|
||||
import { iterateList } from "./utils";
|
||||
|
||||
export class BaseViewModel {
|
||||
protected readonly page: Page;
|
||||
protected readonly list: Locator;
|
||||
|
||||
constructor(page: Page, pageId: string) {
|
||||
this.page = page;
|
||||
this.list = page.locator(`#${pageId} >> ${getTestId("note-list")}`);
|
||||
}
|
||||
|
||||
async findGroup(groupName: string) {
|
||||
const locator = this.list.locator(
|
||||
`${getTestId(`virtuoso-item-list`)} >> ${getTestId("group-header")}`
|
||||
);
|
||||
|
||||
for await (const item of iterateList(locator)) {
|
||||
if ((await item.locator(getTestId("title")).textContent()) === groupName)
|
||||
return item;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected async *iterateItems() {
|
||||
const locator = this.list.locator(
|
||||
`${getTestId(`virtuoso-item-list`)} >> ${getTestId("list-item")}`
|
||||
);
|
||||
for await (const _item of iterateList(locator)) {
|
||||
const id = await _item.getAttribute("id");
|
||||
if (!id) return;
|
||||
|
||||
yield this.list.locator(`#${id}`);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async waitForItem(title: string) {
|
||||
await this.list.locator(getTestId("title"), { hasText: title }).waitFor();
|
||||
}
|
||||
}
|
||||
161
apps/web/__e2e__/models/checkout.model.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2022 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Locator, Page } from "@playwright/test";
|
||||
import { getTestId } from "../utils";
|
||||
import { PriceItem } from "./types";
|
||||
import { iterateList } from "./utils";
|
||||
|
||||
export class CheckoutModel {
|
||||
private readonly page: Page;
|
||||
private readonly seeAllPlansButton: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.seeAllPlansButton = page.locator(getTestId("see-all-plans"));
|
||||
}
|
||||
|
||||
async goto(period?: "monthly" | "yearly", coupon?: string) {
|
||||
let url = "/notes/#/buy/";
|
||||
if (period) url += period;
|
||||
if (coupon) url += `/${coupon}`;
|
||||
await this.page.goto(url);
|
||||
if (!period && !coupon) await this.seeAllPlansButton.waitFor();
|
||||
}
|
||||
|
||||
async getPlans() {
|
||||
await this.seeAllPlansButton.click();
|
||||
|
||||
const plans: Plan[] = [];
|
||||
for await (const item of iterateList(
|
||||
this.page.locator(getTestId("checkout-plan"))
|
||||
)) {
|
||||
plans.push(new Plan(item));
|
||||
}
|
||||
return plans;
|
||||
}
|
||||
|
||||
async getPricing() {
|
||||
const pricing = new PricingModel(this.page);
|
||||
await pricing.waitFor();
|
||||
return pricing;
|
||||
}
|
||||
}
|
||||
|
||||
class Plan {
|
||||
private readonly titleText: Locator;
|
||||
constructor(private readonly locator: Locator) {
|
||||
this.titleText = locator.locator(getTestId("title"));
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
return this.titleText.textContent();
|
||||
}
|
||||
|
||||
async open() {
|
||||
await this.locator.click();
|
||||
const pricing = new PricingModel(this.locator.page());
|
||||
await pricing.waitFor();
|
||||
return pricing;
|
||||
}
|
||||
}
|
||||
|
||||
class PricingModel {
|
||||
private readonly title: Locator;
|
||||
private readonly changePlanButton: Locator;
|
||||
private readonly couponInput: Locator;
|
||||
constructor(private readonly page: Page) {
|
||||
this.title = page.locator(getTestId("checkout-plan-title"));
|
||||
this.changePlanButton = page.locator(getTestId("checkout-plan-change"));
|
||||
this.couponInput = page.locator(getTestId("checkout-coupon-code"));
|
||||
}
|
||||
|
||||
async waitFor() {
|
||||
await this.changePlanButton.waitFor();
|
||||
}
|
||||
|
||||
async waitForCoupon() {
|
||||
await getPaddleFrame(this.page);
|
||||
|
||||
await this.page.locator(getTestId(`checkout-plan-coupon-applied`)).waitFor({
|
||||
state: "attached"
|
||||
});
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
return this.title.textContent();
|
||||
}
|
||||
|
||||
async getPrices() {
|
||||
const prices: PriceItem[] = [];
|
||||
for await (const item of iterateList(
|
||||
this.page.locator(getTestId("checkout-price-item"))
|
||||
)) {
|
||||
const label = await item.locator(getTestId("label")).innerText();
|
||||
const value = await item.locator(getTestId("value")).innerText();
|
||||
prices.push({ label, value });
|
||||
}
|
||||
return prices;
|
||||
}
|
||||
|
||||
async applyCoupon(couponCode: string) {
|
||||
await this.couponInput.fill(couponCode);
|
||||
await this.couponInput.press("Enter");
|
||||
await this.waitForCoupon();
|
||||
}
|
||||
|
||||
async changeCountry(countryCode: string, pinCode?: number) {
|
||||
const paddle = await getPaddleFrame(this.page);
|
||||
|
||||
await paddle
|
||||
.locator(getPaddleTestId("countriesSelect"))
|
||||
.selectOption(countryCode, { force: true });
|
||||
|
||||
if (pinCode)
|
||||
await paddle
|
||||
.locator(getPaddleTestId("postcodeInput"))
|
||||
.fill(pinCode.toString());
|
||||
|
||||
await paddle.locator(getPaddleTestId("locationFormSubmitButton")).click();
|
||||
|
||||
await paddle
|
||||
.locator(getPaddleTestId("inlineComplianceBarContainer"))
|
||||
.waitFor();
|
||||
|
||||
await this.page
|
||||
.locator(getTestId(`checkout-plan-country-${countryCode}`))
|
||||
.waitFor({ state: "attached" });
|
||||
}
|
||||
|
||||
async changePlan() {
|
||||
await this.changePlanButton.click();
|
||||
}
|
||||
}
|
||||
|
||||
function getPaddleTestId(id: string) {
|
||||
return `[data-testid="${id}"]`;
|
||||
}
|
||||
|
||||
async function getPaddleFrame(page: Page) {
|
||||
const paddleFrame = page.frameLocator(`.checkout-container iframe`);
|
||||
await paddleFrame
|
||||
.locator(getPaddleTestId("inlineComplianceBarContainer"))
|
||||
.waitFor();
|
||||
return paddleFrame;
|
||||
}
|
||||
@@ -17,31 +17,25 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const { getTestId } = require(".");
|
||||
import { Page, Locator } from "@playwright/test";
|
||||
import { getTestId } from "../utils";
|
||||
|
||||
class MenuItemIDBuilder {
|
||||
static new(type) {
|
||||
return new MenuItemIDBuilder(type);
|
||||
}
|
||||
constructor(type) {
|
||||
this.type = type;
|
||||
this.suffix = "";
|
||||
export class ContextMenuModel {
|
||||
constructor(private readonly page: Page) {}
|
||||
|
||||
async open(locator: Locator) {
|
||||
await locator.click({ button: "right" });
|
||||
}
|
||||
|
||||
item(itemId) {
|
||||
this.itemId = itemId;
|
||||
return this;
|
||||
async clickOnItem(id: string) {
|
||||
await this.getItem(id).click();
|
||||
}
|
||||
|
||||
checked() {
|
||||
this.suffix = "checked";
|
||||
return this;
|
||||
getItem(id: string) {
|
||||
return this.page.locator(getTestId(`menuitem-${id}`));
|
||||
}
|
||||
|
||||
build() {
|
||||
return getTestId(
|
||||
`${this.type}-${this.itemId}${this.suffix ? `-${this.suffix}` : ""}`
|
||||
);
|
||||
async close() {
|
||||
await this.page.keyboard.press("Escape");
|
||||
}
|
||||
}
|
||||
module.exports = MenuItemIDBuilder;
|
||||
240
apps/web/__e2e__/models/editor.model.ts
Normal file
@@ -0,0 +1,240 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2022 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Locator, Page } from "@playwright/test";
|
||||
import { getTestId } from "../utils";
|
||||
|
||||
export class EditorModel {
|
||||
private readonly page: Page;
|
||||
private readonly title: Locator;
|
||||
private readonly content: Locator;
|
||||
private readonly tags: Locator;
|
||||
private readonly tagInput: Locator;
|
||||
private readonly focusModeButton: Locator;
|
||||
private readonly normalModeButton: Locator;
|
||||
private readonly darkModeButton: Locator;
|
||||
private readonly lightModeButton: Locator;
|
||||
private readonly enterFullscreenButton: Locator;
|
||||
private readonly exitFullscreenButton: Locator;
|
||||
private readonly wordCountText: Locator;
|
||||
private readonly dateEditedText: Locator;
|
||||
private readonly searchButton: Locator;
|
||||
private readonly previewCancelButton: Locator;
|
||||
private readonly previewRestoreButton: Locator;
|
||||
private readonly previewNotice: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.title = page.locator(getTestId("editor-title"));
|
||||
this.content = page.locator(".ProseMirror");
|
||||
this.tagInput = page.locator(getTestId("editor-tag-input"));
|
||||
this.tags = page.locator(`${getTestId("tags")} > ${getTestId("tag")}`);
|
||||
this.focusModeButton = page.locator(getTestId("Focus mode"));
|
||||
this.normalModeButton = page.locator(getTestId("Normal mode"));
|
||||
this.darkModeButton = page.locator(getTestId("Dark mode"));
|
||||
this.lightModeButton = page.locator(getTestId("Light mode"));
|
||||
this.enterFullscreenButton = page.locator(getTestId("Enter fullscreen"));
|
||||
this.exitFullscreenButton = page.locator(getTestId("Exit fullscreen"));
|
||||
this.wordCountText = page.locator(getTestId("editor-word-count"));
|
||||
this.dateEditedText = page.locator(getTestId("editor-date-edited"));
|
||||
this.searchButton = page.locator(getTestId("Search"));
|
||||
this.previewNotice = page.locator(getTestId("preview-notice"));
|
||||
this.previewCancelButton = page.locator(getTestId("preview-notice-cancel"));
|
||||
this.previewRestoreButton = page.locator(
|
||||
getTestId("preview-notice-restore")
|
||||
);
|
||||
}
|
||||
|
||||
async waitForLoading(title?: string, content?: string) {
|
||||
await this.title.waitFor();
|
||||
|
||||
await this.page.waitForFunction(
|
||||
({ expected }) => {
|
||||
const titleInput = document.querySelector(
|
||||
`[data-test-id="editor-title"]`
|
||||
) as HTMLInputElement | null;
|
||||
if (titleInput)
|
||||
return expected !== undefined
|
||||
? titleInput.value === expected
|
||||
: titleInput.value.length > 0;
|
||||
return false;
|
||||
},
|
||||
{ expected: title }
|
||||
);
|
||||
|
||||
if (content !== undefined)
|
||||
await this.content.locator(":scope", { hasText: content }).waitFor();
|
||||
}
|
||||
|
||||
async waitForUnloading() {
|
||||
await this.page.waitForURL(/#\/notes\/create\/\d+/gm);
|
||||
await this.searchButton.waitFor({ state: "hidden" });
|
||||
await this.page.locator(getTestId("tags")).waitFor({ state: "hidden" });
|
||||
await this.wordCountText.waitFor({ state: "hidden" });
|
||||
await this.waitForLoading("", "");
|
||||
}
|
||||
|
||||
async waitForSaving() {
|
||||
await this.page.waitForURL(/#\/notes\/?.+\/edit/gm);
|
||||
await this.page.locator(getTestId("tags")).waitFor();
|
||||
await this.searchButton.waitFor();
|
||||
await this.wordCountText.waitFor();
|
||||
}
|
||||
|
||||
async isUnloaded() {
|
||||
return (
|
||||
(await this.tagInput.isHidden()) &&
|
||||
(await this.darkModeButton.isHidden()) &&
|
||||
(await this.enterFullscreenButton.isHidden()) &&
|
||||
(await this.wordCountText.isHidden())
|
||||
);
|
||||
}
|
||||
|
||||
async setTitle(text: string) {
|
||||
await this.editAndWait(async () => {
|
||||
await this.title.fill(text);
|
||||
});
|
||||
}
|
||||
|
||||
async typeTitle(text: string, delay = 0) {
|
||||
await this.editAndWait(async () => {
|
||||
await this.title.focus();
|
||||
await this.title.type(text, { delay });
|
||||
});
|
||||
}
|
||||
|
||||
async setContent(text: string) {
|
||||
await this.editAndWait(async () => {
|
||||
await this.content.focus();
|
||||
await this.content.type(text);
|
||||
});
|
||||
}
|
||||
|
||||
private async editAndWait(action: () => Promise<void>) {
|
||||
const oldDateEdited = await this.getDateEdited();
|
||||
await action();
|
||||
await this.page.waitForFunction(
|
||||
({ oldDateEdited }) => {
|
||||
const dateEditedText = document.querySelector(
|
||||
`[data-test-id="editor-date-edited"]`
|
||||
);
|
||||
const timestampText = dateEditedText?.getAttribute("title");
|
||||
if (!timestampText) return false;
|
||||
const timestamp = parseInt(timestampText);
|
||||
return timestamp > oldDateEdited;
|
||||
},
|
||||
{ oldDateEdited }
|
||||
);
|
||||
}
|
||||
|
||||
async getDateEdited() {
|
||||
if (await this.dateEditedText.isHidden()) return 0;
|
||||
|
||||
const timestamp = await this.dateEditedText.getAttribute("title");
|
||||
if (timestamp) return parseInt(timestamp);
|
||||
return 0;
|
||||
}
|
||||
|
||||
async selectAll() {
|
||||
await this.content.focus();
|
||||
await this.page.keyboard.press("Home");
|
||||
await this.page.keyboard.press("Shift+End");
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
async setTags(tags: string[]) {
|
||||
for (const tag of tags) {
|
||||
await this.tagInput.fill(tag);
|
||||
await this.tagInput.press("Enter");
|
||||
}
|
||||
}
|
||||
|
||||
async getTags() {
|
||||
const tags: string[] = [];
|
||||
const count = await this.tags.count();
|
||||
for (let i = 0; i < count; ++i) {
|
||||
const item = this.tags.nth(i);
|
||||
const tag = await item.textContent();
|
||||
if (tag) tags.push(tag);
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
async getTitle() {
|
||||
return this.title.inputValue();
|
||||
}
|
||||
|
||||
async getContent(format: "html" | "text") {
|
||||
return format === "html"
|
||||
? await this.content.innerHTML()
|
||||
: (await this.content.innerText()).trim().replace(/\n+/gm, "\n");
|
||||
}
|
||||
|
||||
async enterFocusMode() {
|
||||
await this.focusModeButton.click();
|
||||
await this.normalModeButton.waitFor();
|
||||
}
|
||||
|
||||
async exitFocusMode() {
|
||||
await this.normalModeButton.click();
|
||||
await this.focusModeButton.waitFor();
|
||||
}
|
||||
|
||||
async isFocusMode() {
|
||||
return await this.normalModeButton.isVisible();
|
||||
}
|
||||
|
||||
async enterDarkMode() {
|
||||
await this.darkModeButton.click();
|
||||
await this.lightModeButton.waitFor();
|
||||
}
|
||||
|
||||
async exitDarkMode() {
|
||||
await this.lightModeButton.click();
|
||||
await this.darkModeButton.waitFor();
|
||||
}
|
||||
|
||||
async isDarkMode() {
|
||||
return await this.lightModeButton.isVisible();
|
||||
}
|
||||
|
||||
async enterFullscreen() {
|
||||
await this.enterFullscreenButton.click();
|
||||
await this.exitFullscreenButton.waitFor();
|
||||
}
|
||||
|
||||
async exitFullscreen() {
|
||||
await this.exitFullscreenButton.click();
|
||||
await this.enterFullscreenButton.waitFor();
|
||||
}
|
||||
|
||||
async isFullscreen() {
|
||||
return await this.exitFullscreenButton.isVisible();
|
||||
}
|
||||
|
||||
async cancelPreview() {
|
||||
await this.previewCancelButton.click();
|
||||
await this.previewNotice.waitFor({ state: "hidden" });
|
||||
}
|
||||
|
||||
async restoreSession() {
|
||||
await this.previewRestoreButton.click();
|
||||
await this.previewNotice.waitFor({ state: "hidden" });
|
||||
}
|
||||
}
|
||||
72
apps/web/__e2e__/models/item.model.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2022 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Locator } from "@playwright/test";
|
||||
import { BaseItemModel } from "./base-item.model";
|
||||
import { ContextMenuModel } from "./context-menu.model";
|
||||
import { NotesViewModel } from "./notes-view.model";
|
||||
import { Item } from "./types";
|
||||
import { fillItemDialog } from "./utils";
|
||||
|
||||
export class ItemModel extends BaseItemModel {
|
||||
private readonly contextMenu: ContextMenuModel;
|
||||
constructor(locator: Locator) {
|
||||
super(locator);
|
||||
this.contextMenu = new ContextMenuModel(this.page);
|
||||
}
|
||||
|
||||
async open() {
|
||||
await this.locator.click();
|
||||
return new NotesViewModel(this.page, "notes");
|
||||
}
|
||||
|
||||
async delete() {
|
||||
await this.contextMenu.open(this.locator);
|
||||
await Promise.all([
|
||||
this.contextMenu.clickOnItem("delete"),
|
||||
this.waitFor("detached")
|
||||
]);
|
||||
}
|
||||
|
||||
async editItem(item: Item) {
|
||||
await this.contextMenu.open(this.locator);
|
||||
await this.contextMenu.clickOnItem("edit");
|
||||
|
||||
await fillItemDialog(this.page, item);
|
||||
}
|
||||
|
||||
async createShortcut() {
|
||||
await this.contextMenu.open(this.locator);
|
||||
await this.contextMenu.clickOnItem("shortcut");
|
||||
}
|
||||
|
||||
async removeShortcut() {
|
||||
await this.contextMenu.open(this.locator);
|
||||
await this.contextMenu.clickOnItem("shortcut");
|
||||
}
|
||||
|
||||
async isShortcut() {
|
||||
await this.contextMenu.open(this.locator);
|
||||
const state =
|
||||
(await this.contextMenu.getItem("shortcut").textContent()) ===
|
||||
"Remove shortcut";
|
||||
await this.contextMenu.close();
|
||||
return state;
|
||||
}
|
||||
}
|
||||
54
apps/web/__e2e__/models/items-view.model.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2022 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Locator, Page } from "@playwright/test";
|
||||
import { getTestId } from "../utils";
|
||||
import { BaseViewModel } from "./base-view.model";
|
||||
import { ItemModel } from "./item.model";
|
||||
import { Item } from "./types";
|
||||
import { fillItemDialog } from "./utils";
|
||||
|
||||
export class ItemsViewModel extends BaseViewModel {
|
||||
private readonly createButton: Locator;
|
||||
|
||||
constructor(page: Page, private readonly id: "topics" | "tags") {
|
||||
super(page, id);
|
||||
this.createButton = page.locator(getTestId(`${id}-action-button`));
|
||||
}
|
||||
|
||||
async createItem(item: Item) {
|
||||
const titleToCompare = this.id === "tags" ? `#${item.title}` : item.title;
|
||||
|
||||
await this.createButton.first().click();
|
||||
await fillItemDialog(this.page, item);
|
||||
|
||||
await this.waitForItem(titleToCompare);
|
||||
return await this.findItem(item);
|
||||
}
|
||||
|
||||
async findItem(item: Item) {
|
||||
const titleToCompare = this.id === "tags" ? `#${item.title}` : item.title;
|
||||
for await (const _item of this.iterateItems()) {
|
||||
const itemModel = new ItemModel(_item);
|
||||
const title = await itemModel.getTitle();
|
||||
if (title === titleToCompare) return itemModel;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
93
apps/web/__e2e__/models/navigation-menu.model.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2022 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Locator, Page } from "@playwright/test";
|
||||
import { getTestId } from "../utils";
|
||||
import { ContextMenuModel } from "./context-menu.model";
|
||||
import { fillItemDialog, iterateList } from "./utils";
|
||||
|
||||
export class NavigationMenuModel {
|
||||
protected readonly page: Page;
|
||||
private readonly menu: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
this.menu = page.locator(getTestId("navigation-menu"));
|
||||
}
|
||||
|
||||
async findItem(title: string) {
|
||||
for await (const item of this.iterateList()) {
|
||||
const menuItem = new NavigationItemModel(item);
|
||||
if ((await menuItem.getTitle()) === title) return menuItem;
|
||||
}
|
||||
}
|
||||
|
||||
async getShortcuts() {
|
||||
const shortcuts: string[] = [];
|
||||
for await (const item of this.iterateList()) {
|
||||
const menuItem = new NavigationItemModel(item);
|
||||
if (await menuItem.isShortcut()) {
|
||||
const titleText = await menuItem.getTitle();
|
||||
if (titleText) shortcuts.push(titleText);
|
||||
}
|
||||
}
|
||||
return shortcuts;
|
||||
}
|
||||
|
||||
private async *iterateList() {
|
||||
const locator = this.menu.locator(getTestId(`navigation-item`));
|
||||
yield* iterateList(locator);
|
||||
}
|
||||
}
|
||||
|
||||
class NavigationItemModel {
|
||||
private readonly title: Locator;
|
||||
private readonly shortcut: Locator;
|
||||
private readonly menu: ContextMenuModel;
|
||||
private readonly page: Page;
|
||||
constructor(private readonly locator: Locator) {
|
||||
this.page = locator.page();
|
||||
this.title = locator.locator(getTestId("title"));
|
||||
this.shortcut = locator.locator(getTestId("shortcut"));
|
||||
this.menu = new ContextMenuModel(this.page);
|
||||
}
|
||||
|
||||
async click() {
|
||||
await this.locator.click();
|
||||
}
|
||||
|
||||
async isShortcut() {
|
||||
return await this.shortcut.isVisible();
|
||||
}
|
||||
|
||||
async getTitle() {
|
||||
return await this.title.textContent();
|
||||
}
|
||||
|
||||
async renameColor(alias: string) {
|
||||
await this.menu.open(this.locator);
|
||||
await this.menu.clickOnItem("rename");
|
||||
await fillItemDialog(this.page, { title: alias });
|
||||
}
|
||||
|
||||
async removeShortcut() {
|
||||
await this.menu.open(this.locator);
|
||||
await this.menu.clickOnItem("remove-shortcut");
|
||||
}
|
||||
}
|
||||
69
apps/web/__e2e__/models/note-item.model.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2022 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Locator } from "@playwright/test";
|
||||
import { getTestId } from "../utils";
|
||||
import { BaseItemModel } from "./base-item.model";
|
||||
import { EditorModel } from "./editor.model";
|
||||
import {
|
||||
NoteContextMenuModel,
|
||||
NotePropertiesModel
|
||||
} from "./note-properties.model";
|
||||
import { iterateList } from "./utils";
|
||||
|
||||
export class NoteItemModel extends BaseItemModel {
|
||||
readonly properties: NotePropertiesModel;
|
||||
readonly contextMenu: NoteContextMenuModel;
|
||||
private readonly editor: EditorModel;
|
||||
constructor(locator: Locator) {
|
||||
super(locator);
|
||||
this.properties = new NotePropertiesModel(this.page, locator);
|
||||
this.contextMenu = new NoteContextMenuModel(this.page, locator);
|
||||
this.editor = new EditorModel(this.page);
|
||||
}
|
||||
|
||||
async openNote() {
|
||||
await this.locator.click();
|
||||
const title = await this.getTitle();
|
||||
const description = await this.getDescription();
|
||||
|
||||
await this.editor.waitForLoading(title, description);
|
||||
}
|
||||
|
||||
async openLockedNote(password: string) {
|
||||
if (!(await this.contextMenu.isLocked())) return;
|
||||
|
||||
await this.page.locator(getTestId("unlock-note-password")).fill(password);
|
||||
await this.page.locator(getTestId("unlock-note-submit")).click();
|
||||
|
||||
const title = await this.getTitle();
|
||||
await this.editor.waitForLoading(title);
|
||||
}
|
||||
|
||||
async getTags() {
|
||||
const tags: string[] = [];
|
||||
for await (const item of iterateList(
|
||||
this.locator.locator(getTestId("tag-item"))
|
||||
)) {
|
||||
const title = await item.textContent();
|
||||
if (title) tags.push(title.replace("#", ""));
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
}
|
||||
333
apps/web/__e2e__/models/note-properties.model.ts
Normal file
@@ -0,0 +1,333 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2022 Streetwriters (Private) Limited
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Locator, Page } from "@playwright/test";
|
||||
import { downloadAndReadFile, getTestId } from "../utils";
|
||||
import { ContextMenuModel } from "./context-menu.model";
|
||||
import { ToggleModel } from "./toggle.model";
|
||||
import { Notebook } from "./types";
|
||||
import { fillPasswordDialog, iterateList } from "./utils";
|
||||
|
||||
abstract class BaseProperties {
|
||||
protected readonly page: Page;
|
||||
|
||||
private readonly pinToggle: ToggleModel;
|
||||
private readonly favoriteToggle: ToggleModel;
|
||||
private readonly lockToggle: ToggleModel;
|
||||
|
||||
constructor(
|
||||
page: Page,
|
||||
protected readonly noteLocator: Locator,
|
||||
private readonly itemPrefix: string
|
||||
) {
|
||||
this.page = page;
|
||||
this.pinToggle = new ToggleModel(page, `${itemPrefix}-pin`);
|
||||
this.lockToggle = new ToggleModel(page, `${itemPrefix}-lock`);
|
||||
this.favoriteToggle = new ToggleModel(page, `${itemPrefix}-favorite`);
|
||||
}
|
||||
|
||||
async isPinned() {
|
||||
await this.open();
|
||||
const state = await this.pinToggle.isToggled();
|
||||
await this.close();
|
||||
return state;
|
||||
}
|
||||
|
||||
async pin() {
|
||||
await this.open();
|
||||
await this.pinToggle.on();
|
||||
await this.close();
|
||||
}
|
||||
|
||||
async unpin() {
|
||||
await this.open();
|
||||
await this.pinToggle.off();
|
||||
await this.close();
|
||||
}
|
||||
|
||||
async lock(password: string) {
|
||||
await this.open();
|
||||
await this.lockToggle.on(false);
|
||||
await fillPasswordDialog(this.page, password);
|
||||
await this.noteLocator
|
||||
.locator(getTestId("locked"))
|
||||
.waitFor({ state: "visible" });
|
||||
}
|
||||
|
||||
async unlock(password: string) {
|
||||
await this.open();
|
||||
await this.lockToggle.off(false);
|
||||
|
||||
await fillPasswordDialog(this.page, password);
|
||||
|
||||
await this.noteLocator
|
||||
.locator(getTestId("locked"))
|
||||
.waitFor({ state: "hidden" });
|
||||
}
|
||||
|
||||
async isLocked() {
|
||||
return (
|
||||
(await this.noteLocator.locator(getTestId("locked")).isVisible()) &&
|
||||
(await this.noteLocator.locator(getTestId(`description`)).isHidden()) &&
|
||||
(await (async () => {
|
||||
await this.noteLocator.click();
|
||||
return await this.page
|
||||
.locator(getTestId("unlock-note-title"))
|
||||
.isVisible();
|
||||
})()) &&
|
||||
(await (async () => {
|
||||
await this.open();
|
||||
const state = await this.lockToggle.isToggled();
|
||||
await this.close();
|
||||
return state;
|
||||
})())
|
||||
);
|
||||
}
|
||||
|
||||
async isFavorited() {
|
||||
await this.open();
|
||||
const state = await this.favoriteToggle.isToggled();
|
||||
await this.close();
|
||||
return state;
|
||||
}
|
||||
|
||||
async favorite() {
|
||||
await this.open();
|
||||
await this.favoriteToggle.on();
|
||||
await this.close();
|
||||
}
|
||||
|
||||
async unfavorite() {
|
||||
await this.open();
|
||||
await this.favoriteToggle.off();
|
||||
await this.close();
|
||||
}
|
||||
|
||||
abstract isColored(color: string): Promise<boolean>;
|
||||
abstract color(color: string): Promise<void>;
|
||||
abstract open(): Promise<void>;
|
||||
abstract close(): Promise<void>;
|
||||
}
|
||||
|
||||
export class NotePropertiesModel extends BaseProperties {
|
||||
private readonly propertiesButton: Locator;
|
||||
private readonly propertiesCloseButton: Locator;
|
||||
private readonly readonlyToggle: ToggleModel;
|
||||
private readonly sessionItems: Locator;
|
||||
|
||||
constructor(page: Page, noteLocator: Locator) {
|
||||
super(page, noteLocator, "properties");
|
||||
this.propertiesButton = page.locator(getTestId("Properties"));
|
||||
this.propertiesCloseButton = page.locator(getTestId("properties-close"));
|
||||
this.readonlyToggle = new ToggleModel(page, `properties-readonly`);
|
||||
this.sessionItems = page.locator(getTestId("session-item"));
|
||||
}
|
||||
|
||||
async isColored(color: string): Promise<boolean> {
|
||||
await this.open();
|
||||
const state = await new ToggleModel(
|
||||
this.page,
|
||||
`properties-${color}`
|
||||
).isToggled();
|
||||
await this.close();
|
||||
return state;
|
||||
}
|
||||
|
||||
async color(color: string) {
|
||||
await this.open();
|
||||
await new ToggleModel(this.page, `properties-${color}`).on();
|
||||
await this.close();
|
||||
}
|
||||
|
||||
async isReadonly() {
|
||||
await this.open();
|
||||
const state = await this.readonlyToggle.isToggled();
|
||||
await this.close();
|
||||
return state;
|
||||
}
|
||||
|
||||
async readonly() {
|
||||
await this.open();
|
||||
await this.readonlyToggle.on();
|
||||
await this.close();
|
||||
}
|
||||
|
||||
async editable() {
|
||||
await this.open();
|
||||
await this.readonlyToggle.off();
|
||||
await this.close();
|
||||
}
|
||||
|
||||
async open() {
|
||||
await this.propertiesButton.click();
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this.propertiesCloseButton.click();
|
||||
}
|
||||
|
||||
async getSessionHistory() {
|
||||
await this.open();
|
||||
const history: SessionHistoryItemModel[] = [];
|
||||
for await (const item of iterateList(this.sessionItems)) {
|
||||
history.push(new SessionHistoryItemModel(this, item));
|
||||
}
|
||||
await this.close();
|
||||
return history;
|
||||
}
|
||||
}
|
||||
|
||||
export class NoteContextMenuModel extends BaseProperties {
|
||||
private readonly menu: ContextMenuModel;
|
||||
constructor(page: Page, noteLocator: Locator) {
|
||||
super(page, noteLocator, "menuitem");
|
||||
this.menu = new ContextMenuModel(page);
|
||||
}
|
||||
|
||||
async isColored(color: string): Promise<boolean> {
|
||||
await this.open();
|
||||
await this.menu.clickOnItem("colors");
|
||||
const state = await new ToggleModel(
|
||||
this.page,
|
||||
`menuitem-${color}`
|
||||
).isToggled();
|
||||
await this.close();
|
||||
return state;
|
||||
}
|
||||
|
||||
async color(color: string) {
|
||||
await this.open();
|
||||
await this.menu.clickOnItem("colors");
|
||||
await new ToggleModel(this.page, `menuitem-${color}`).on();
|
||||
await this.close();
|
||||
}
|
||||
|
||||
async moveToTrash() {
|
||||
await this.open();
|
||||
await Promise.all([
|
||||
this.menu.clickOnItem("movetotrash"),
|
||||
this.noteLocator.waitFor({ state: "detached" })
|
||||
]);
|
||||
}
|
||||
|
||||
async export(format: "html" | "md" | "txt") {
|
||||
await this.open();
|
||||
await this.menu.clickOnItem("export");
|
||||
|
||||
// we need to override date time so
|
||||
// date created & date edited remain fixed.
|
||||
await this.noteLocator.evaluate(() => {
|
||||
// eslint-disable-next-line no-extend-native
|
||||
Date.prototype.toLocaleString = () => "xxx";
|
||||
});
|
||||
|
||||
return await downloadAndReadFile(
|
||||
this.noteLocator.page(),
|
||||
this.menu.getItem(format),
|
||||
"utf-8"
|
||||
);
|
||||
}
|
||||
|
||||
async addToNotebook(notebook: Notebook) {
|
||||
await this.open();
|
||||
await this.menu.clickOnItem("addtonotebook");
|
||||
|
||||
const filterInput = this.page.locator(getTestId("filter-input"));
|
||||
await filterInput.type(notebook.title);
|
||||
await filterInput.press("Enter");
|
||||
|
||||
await this.page.waitForSelector(getTestId("notebook"), {
|
||||
state: "visible",
|
||||
strict: false
|
||||
});
|
||||
|
||||
const notebookItems = this.page.locator(getTestId("notebook"));
|
||||
for await (const item of iterateList(notebookItems)) {
|
||||
const treeItem = item.locator(getTestId(`tree-item`));
|
||||
const title = treeItem.locator(getTestId("title"));
|
||||
const newItemButton = treeItem.locator(getTestId("tree-item-new"));
|
||||
|
||||
if ((await title.textContent()) === notebook.title) {
|
||||
for (const topic of notebook.topics) {
|
||||
await newItemButton.click();
|
||||
const newItemInput = item.locator(getTestId("new-tree-item-input"));
|
||||
|
||||
await newItemInput.waitFor({ state: "visible" });
|
||||
await newItemInput.fill(topic);
|
||||
await newItemInput.press("Enter");
|
||||
}
|
||||
|
||||
const topicItems = item.locator(getTestId("topic"));
|
||||
for await (const topicItem of iterateList(topicItems)) {
|
||||
const treeItem = topicItem.locator(getTestId(`tree-item`));
|
||||
await treeItem.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const dialogConfirm = this.page.locator(getTestId("dialog-yes"));
|
||||
await dialogConfirm.click();
|
||||
await dialogConfirm.waitFor({ state: "detached" });
|
||||
}
|
||||
|
||||
async open() {
|
||||
await this.menu.open(this.noteLocator);
|
||||
}
|
||||
|
||||
async close() {
|
||||
await this.menu.close();
|
||||
}
|
||||
}
|
||||
|
||||
class SessionHistoryItemModel {
|
||||
private readonly title: Locator;
|
||||
private readonly page: Page;
|
||||
private readonly previewNotice: Locator;
|
||||
private readonly locked: Locator;
|
||||
constructor(
|
||||
private readonly properties: NotePropertiesModel,
|
||||
private readonly locator: Locator
|
||||
) {
|
||||
this.page = locator.page();
|
||||
this.title = locator.locator(getTestId("title"));
|
||||
this.previewNotice = this.page.locator(getTestId("preview-notice"));
|
||||
this.locked = locator.locator(getTestId("locked"));
|
||||
}
|
||||
|
||||
async getTitle() {
|
||||
return await this.title.textContent();
|
||||
}
|
||||
|
||||
async preview(password?: string) {
|
||||
await this.properties.open();
|
||||
const isLocked = await this.locked.isVisible();
|
||||
await this.locator.click();
|
||||
if (password && isLocked) {
|
||||
await fillPasswordDialog(this.page, password);
|
||||
}
|
||||
await this.previewNotice.waitFor();
|
||||
}
|
||||
|
||||
async isLocked() {
|
||||
await this.properties.open();
|
||||
const state = await this.locked.isVisible();
|
||||
await this.properties.close();
|
||||
return state;
|
||||
}
|
||||
}
|
||||