From 3ccc2a9984c1a03629c49e51927519da1a222917 Mon Sep 17 00:00:00 2001 From: Peter Salomonsen Date: Wed, 10 Jul 2024 10:16:55 +0000 Subject: [PATCH 1/9] scroll proposal comment links into view --- .../entity/proposal/CommentsAndLogs.jsx | 1 + playwright-tests/tests/proposal/links.spec.js | 82 +++++++++++++++++++ .../tests/proposal/proposals.spec.js | 48 ----------- 3 files changed, 83 insertions(+), 48 deletions(-) create mode 100644 playwright-tests/tests/proposal/links.spec.js diff --git a/instances/devhub.near/widget/devhub/entity/proposal/CommentsAndLogs.jsx b/instances/devhub.near/widget/devhub/entity/proposal/CommentsAndLogs.jsx index 4ce7a83a6..8c914ba0a 100644 --- a/instances/devhub.near/widget/devhub/entity/proposal/CommentsAndLogs.jsx +++ b/instances/devhub.near/widget/devhub/entity/proposal/CommentsAndLogs.jsx @@ -178,6 +178,7 @@ const Comment = ({ commentItem }) => { /> diff --git a/playwright-tests/tests/proposal/links.spec.js b/playwright-tests/tests/proposal/links.spec.js new file mode 100644 index 000000000..cee75d210 --- /dev/null +++ b/playwright-tests/tests/proposal/links.spec.js @@ -0,0 +1,82 @@ +import test, { expect } from "@playwright/test"; + +test.describe("share links", () => { + test.use({ + contextOptions: { + permissions: ["clipboard-read", "clipboard-write"], + }, + }); + test("copy link button should create a clean URL link", async ({ page }) => { + await page.goto("/devhub.near/widget/app?page=proposal&id=127"); + + await expect(await page.getByText("#127")).toBeVisible(); + const shareLinkButton = await page.getByRole("button", { name: "" }); + await shareLinkButton.click(); + await page.getByRole("button", { name: "Copy link to proposal" }).click(); + + const linkUrlFromClipboard = await page.evaluate( + "navigator.clipboard.readText()" + ); + expect(linkUrlFromClipboard).toEqual( + "https://devhub.near.page/proposal/127" + ); + await pauseIfVideoRecording(page); + await page.goto(linkUrlFromClipboard); + + await expect(await page.getByText("#127")).toBeVisible({ + timeout: 10000, + }); + }); + + test("share on X should create a clean URL link", async ({ + page, + context, + }) => { + await page.goto("/devhub.near/widget/app?page=proposal&id=127"); + + await expect(await page.getByText("#127")).toBeVisible(); + const shareLinkButton = await page.getByRole("button", { name: "" }); + await shareLinkButton.click(); + const shareOnXLink = await page.getByRole("link", { name: " Share on X" }); + const shareOnXUrl = await shareOnXLink.getAttribute("href"); + await expect(shareOnXUrl).toEqual( + "https://x.com/intent/post?text=Check+out+this+proposal+on+%40NEARProtocol%0A%23NEAR+%23BOS%0Ahttps%3A%2F%2Fdevhub.near.page%2Fproposal%2F127" + ); + await shareOnXLink.click(); + const twitterPage = await context.waitForEvent("page"); + await twitterPage.waitForURL(shareOnXUrl); + }); + + test("copying links should have clean URLs and scroll into view", async ({ + page, + }) => { + await page.goto( + "/devhub.near/widget/app?page=proposal&id=127#theorinear_121684702" + ); + await page.evaluate(() => { + (async function () { + if (location.hash) { + for (let n = 0; n < 20; n++) { + const linkElement = document.querySelector(location.hash); + console.log( + "waiting for target element to appear", + location.hash, + n + ); + if (linkElement) { + linkElement.scrollIntoView(); + console.log("scrolled into view"); + break; + } + await new Promise((resolve) => setTimeout(() => resolve(), 500)); + } + } + })(); + }); + const viewer = await page.locator("near-social-viewer"); + const commentElement = await viewer.locator("css=div#theorinear_121684702"); + await expect(commentElement).toBeVisible(); + + await expect(commentElement).toBeInViewport({ timeout: 10000 }); + }); +}); diff --git a/playwright-tests/tests/proposal/proposals.spec.js b/playwright-tests/tests/proposal/proposals.spec.js index fe0b5b4c9..0188365a2 100644 --- a/playwright-tests/tests/proposal/proposals.spec.js +++ b/playwright-tests/tests/proposal/proposals.spec.js @@ -742,51 +742,3 @@ test.describe("Wallet is connected", () => { }); }); }); - -test.describe("share links", () => { - test.use({ - contextOptions: { - permissions: ["clipboard-read", "clipboard-write"], - }, - }); - test("copy link button should create a clean URL link", async ({ page }) => { - await page.goto("/devhub.near/widget/app?page=proposal&id=127"); - - await expect(await page.getByText("#127")).toBeVisible(); - const shareLinkButton = await page.getByRole("button", { name: "" }); - await shareLinkButton.click(); - await page.getByRole("button", { name: "Copy link to proposal" }).click(); - - const linkUrlFromClipboard = await page.evaluate( - "navigator.clipboard.readText()" - ); - expect(linkUrlFromClipboard).toEqual( - "https://devhub.near.page/proposal/127" - ); - await pauseIfVideoRecording(page); - await page.goto(linkUrlFromClipboard); - - await expect(await page.getByText("#127")).toBeVisible({ - timeout: 10000, - }); - }); - - test("share on X should create a clean URL link", async ({ - page, - context, - }) => { - await page.goto("/devhub.near/widget/app?page=proposal&id=127"); - - await expect(await page.getByText("#127")).toBeVisible(); - const shareLinkButton = await page.getByRole("button", { name: "" }); - await shareLinkButton.click(); - const shareOnXLink = await page.getByRole("link", { name: " Share on X" }); - const shareOnXUrl = await shareOnXLink.getAttribute("href"); - await expect(shareOnXUrl).toEqual( - "https://x.com/intent/post?text=Check+out+this+proposal+on+%40NEARProtocol%0A%23NEAR+%23BOS%0Ahttps%3A%2F%2Fdevhub.near.page%2Fproposal%2F127" - ); - await shareOnXLink.click(); - const twitterPage = await context.waitForEvent("page"); - await twitterPage.waitForURL(shareOnXUrl); - }); -}); From 639aca1592545fbe719e8662b2dbcb57977b69f0 Mon Sep 17 00:00:00 2001 From: Peter Salomonsen Date: Wed, 10 Jul 2024 10:59:16 +0000 Subject: [PATCH 2/9] use URL search params for comment link since the existing highlight functionality depends on getting accountId and blockheight through props --- .../devhub/entity/proposal/CommentsAndLogs.jsx | 6 ++---- playwright-tests/tests/proposal/links.spec.js | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/instances/devhub.near/widget/devhub/entity/proposal/CommentsAndLogs.jsx b/instances/devhub.near/widget/devhub/entity/proposal/CommentsAndLogs.jsx index 8c914ba0a..8c5889bcf 100644 --- a/instances/devhub.near/widget/devhub/entity/proposal/CommentsAndLogs.jsx +++ b/instances/devhub.near/widget/devhub/entity/proposal/CommentsAndLogs.jsx @@ -159,9 +159,7 @@ const Comment = ({ commentItem }) => { blockHeight, }; const content = JSON.parse(Social.get(item.path, blockHeight) ?? "null"); - const link = getLinkUsingCurrentGateway( - `${REPL_DEVHUB}/widget/app?page=proposal&id=${props.id}&accountId=${accountId}&blockHeight=${blockHeight}` - ); + const link = `https://${REPL_DEVHUB}.page/proposal/${proposal.id}?accountId=${accountId}&blockHeight=${blockHeight}`; const hightlightComment = parseInt(props.blockHeight ?? "") === blockHeight && props.accountId === accountId; @@ -178,7 +176,7 @@ const Comment = ({ commentItem }) => { /> diff --git a/playwright-tests/tests/proposal/links.spec.js b/playwright-tests/tests/proposal/links.spec.js index cee75d210..9c78e3c1a 100644 --- a/playwright-tests/tests/proposal/links.spec.js +++ b/playwright-tests/tests/proposal/links.spec.js @@ -51,16 +51,23 @@ test.describe("share links", () => { page, }) => { await page.goto( - "/devhub.near/widget/app?page=proposal&id=127#theorinear_121684702" + "/devhub.near/widget/app?page=proposal&id=127&accountId=theori.near&blockHeight=121684702" ); await page.evaluate(() => { (async function () { - if (location.hash) { + const urlSearchParams = new URLSearchParams(location.search); + const accountId = urlSearchParams.get("accountId"); + const blockHeight = urlSearchParams.get("blockHeight"); + if (accountId && blockHeight) { for (let n = 0; n < 20; n++) { - const linkElement = document.querySelector(location.hash); + const linkElementSelector = `#${accountId.replace( + /[^a-z0-9]/g, + "" + )}${blockHeight}`; + const linkElement = document.querySelector(linkElementSelector); console.log( "waiting for target element to appear", - location.hash, + linkElementSelector, n ); if (linkElement) { @@ -74,7 +81,7 @@ test.describe("share links", () => { })(); }); const viewer = await page.locator("near-social-viewer"); - const commentElement = await viewer.locator("css=div#theorinear_121684702"); + const commentElement = await viewer.locator("css=div#theorinear121684702"); await expect(commentElement).toBeVisible(); await expect(commentElement).toBeInViewport({ timeout: 10000 }); From 7ad7ce7df456847c3e52f3548a1480f744322861 Mon Sep 17 00:00:00 2001 From: Peter Salomonsen Date: Wed, 10 Jul 2024 15:04:12 +0000 Subject: [PATCH 3/9] add missing pauseIfVideorecording import --- playwright-tests/tests/proposal/links.spec.js | 9 +++------ playwright-tests/tests/proposal/proposals.spec.js | 1 - 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/playwright-tests/tests/proposal/links.spec.js b/playwright-tests/tests/proposal/links.spec.js index 9c78e3c1a..74083a212 100644 --- a/playwright-tests/tests/proposal/links.spec.js +++ b/playwright-tests/tests/proposal/links.spec.js @@ -1,4 +1,5 @@ import test, { expect } from "@playwright/test"; +import { pauseIfVideoRecording } from "../../testUtils.js"; test.describe("share links", () => { test.use({ @@ -47,7 +48,7 @@ test.describe("share links", () => { await twitterPage.waitForURL(shareOnXUrl); }); - test("copying links should have clean URLs and scroll into view", async ({ + test("copying comment links should have clean URLs and scroll into view", async ({ page, }) => { await page.goto( @@ -64,12 +65,8 @@ test.describe("share links", () => { /[^a-z0-9]/g, "" )}${blockHeight}`; + const linkElement = document.querySelector(linkElementSelector); - console.log( - "waiting for target element to appear", - linkElementSelector, - n - ); if (linkElement) { linkElement.scrollIntoView(); console.log("scrolled into view"); diff --git a/playwright-tests/tests/proposal/proposals.spec.js b/playwright-tests/tests/proposal/proposals.spec.js index 0188365a2..8b9e79416 100644 --- a/playwright-tests/tests/proposal/proposals.spec.js +++ b/playwright-tests/tests/proposal/proposals.spec.js @@ -11,7 +11,6 @@ import { encodeResultJSON, } from "../../util/transaction.js"; import { mockRpcRequest } from "../../util/rpcmock.js"; -import { mockSocialIndexResponses } from "../../util/socialapi.js"; test.afterEach( async ({ page }) => await page.unrouteAll({ behavior: "ignoreErrors" }) From be9b12b06c3d55aab77d24565fddeb013ae593bc Mon Sep 17 00:00:00 2001 From: Peter Salomonsen Date: Wed, 10 Jul 2024 16:59:10 +0000 Subject: [PATCH 4/9] add missing proposal id --- .../entity/proposal/CommentsAndLogs.jsx | 2 +- playwright-tests/tests/proposal/links.spec.js | 26 ++++++++++++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/instances/devhub.near/widget/devhub/entity/proposal/CommentsAndLogs.jsx b/instances/devhub.near/widget/devhub/entity/proposal/CommentsAndLogs.jsx index 8c5889bcf..5f7b55a70 100644 --- a/instances/devhub.near/widget/devhub/entity/proposal/CommentsAndLogs.jsx +++ b/instances/devhub.near/widget/devhub/entity/proposal/CommentsAndLogs.jsx @@ -159,7 +159,7 @@ const Comment = ({ commentItem }) => { blockHeight, }; const content = JSON.parse(Social.get(item.path, blockHeight) ?? "null"); - const link = `https://${REPL_DEVHUB}.page/proposal/${proposal.id}?accountId=${accountId}&blockHeight=${blockHeight}`; + const link = `https://${REPL_DEVHUB}.page/proposal/${proposalId}?accountId=${accountId}&blockHeight=${blockHeight}`; const hightlightComment = parseInt(props.blockHeight ?? "") === blockHeight && props.accountId === accountId; diff --git a/playwright-tests/tests/proposal/links.spec.js b/playwright-tests/tests/proposal/links.spec.js index 74083a212..a2c512f4b 100644 --- a/playwright-tests/tests/proposal/links.spec.js +++ b/playwright-tests/tests/proposal/links.spec.js @@ -48,9 +48,7 @@ test.describe("share links", () => { await twitterPage.waitForURL(shareOnXUrl); }); - test("copying comment links should have clean URLs and scroll into view", async ({ - page, - }) => { + test("comment links should scroll into view", async ({ page }) => { await page.goto( "/devhub.near/widget/app?page=proposal&id=127&accountId=theori.near&blockHeight=121684702" ); @@ -83,4 +81,26 @@ test.describe("share links", () => { await expect(commentElement).toBeInViewport({ timeout: 10000 }); }); + + test("copying comment links should have clean URLs", async ({ page }) => { + await page.goto( + "/devhub.near/widget/app?page=proposal&id=127&accountId=theori.near&blockHeight=121684702" + ); + const viewer = await page.locator("near-social-viewer"); + const commentElement = await viewer.locator("css=div#theorinear121684702"); + await expect(commentElement).toBeVisible(); + await page + .locator("#theorinear121684702") + .getByLabel("Copy URL to clipboard") + .click(); + + const linkUrlFromClipboard = await page.evaluate( + "navigator.clipboard.readText()" + ); + expect(linkUrlFromClipboard).toEqual( + "https://devhub.near.page/proposal/127?accountId=theori.near&blockHeight=121684702" + ); + await pauseIfVideoRecording(page); + await page.goto(linkUrlFromClipboard); + }); }); From 5126f5a649bcbfcdd1b7cb0ed2a26ffa92d9b169 Mon Sep 17 00:00:00 2001 From: Peter Salomonsen Date: Thu, 11 Jul 2024 06:45:13 +0000 Subject: [PATCH 5/9] building of preview env script, updated docs on building preview environments --- CONTRIBUTING.md | 11 ++++++++++- package.json | 2 +- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a5e41b6bd..cf53db1ac 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -70,8 +70,17 @@ Feel free to specify a new placeholder if needed. The placeholder should have a #### Deploying preview environments -Refer to [BOS-workspace deployment instructions](https://github.com/NEARBuilders/bos-workspace?tab=readme-ov-file#deployment) for deploying preview environments. +Creating a preview environment is often useful for reviewers to test the new features before deploying to production. Before we can deploy components, there are several replacements of global variables that needs to be made, which is handled by the BOS workspaces build command. +By running: + +```bash +npm run bw:build:devhub +``` + +you will get a new folder `build/devhub.near` that contains a `src` folder with all the components ready for deployment. The replacements are made according to the settings in `instances/devhub.near/aliases.mainnet.json` so you should for example change `REPL_DEVHUB` to the account name you are using for your preview environment. Note that "BOS workspaces" has a built in connection to BOS CLI for deploying with one command, but since "BOS workspaces" will output to `src/widgets` instead of `src`, and that it requires the signing key as a command argument, we have chosen to split this process in two steps using `bw build ` for replacing and preparing the deployment folder, and using `bos components deploy` separately for the deployment. + +From the `build/devhub.near` folder you can use `bos components deploy`. #### Deploy for Production diff --git a/package.json b/package.json index 7abf7e86f..9a7a83d54 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "fmt:check": "prettier --check '**/*.{js,jsx,ts,tsx,json}'", "build": "npm run fmt", "bw:dev:devhub": "bw dev instances/devhub.near", - "bw:build:devhub": "bw build instances/devhub.near", + "bw:build:devhub": "bw build instances/devhub.near build/devhub.near && mv build/devhub.near/src/widget/* build/devhub.near/src/ && rm -Rf build/devhub.near/src/widget", "gateway:devhub": "node scripts/dev-gateway.mjs devhub.near", "bw:dev:infrastructure": "bw dev instances/infrastructure-committee.near", "bw:build:infrastructure": "bw build instances/infrastructure-committee.near", From a0a94b64aea6d39bfb900a73bb74260e99c5a0d1 Mon Sep 17 00:00:00 2001 From: Peter Salomonsen Date: Thu, 11 Jul 2024 10:29:38 +0000 Subject: [PATCH 6/9] blogv2 tests split into multiple files. added timeouts to long running tests in CI --- .../blog/blogv2.admin.dontaskagain.spec.js | 505 ++++++++++ .../tests/blog/blogv2.admin.spec.js | 429 +++++++++ playwright-tests/tests/blog/blogv2.spec.js | 897 +----------------- 3 files changed, 935 insertions(+), 896 deletions(-) create mode 100644 playwright-tests/tests/blog/blogv2.admin.dontaskagain.spec.js create mode 100644 playwright-tests/tests/blog/blogv2.admin.spec.js diff --git a/playwright-tests/tests/blog/blogv2.admin.dontaskagain.spec.js b/playwright-tests/tests/blog/blogv2.admin.dontaskagain.spec.js new file mode 100644 index 000000000..21eb54851 --- /dev/null +++ b/playwright-tests/tests/blog/blogv2.admin.dontaskagain.spec.js @@ -0,0 +1,505 @@ +import { expect, test } from "@playwright/test"; +import { pauseIfVideoRecording } from "../../testUtils.js"; +import { mockDefaultTabs } from "../../util/addons.js"; +import { mockBlogs } from "../../util/blogs.js"; +import { setDontAskAgainCacheValues } from "../../util/cache.js"; +import { + mockTransactionSubmitRPCResponses, + decodeResultJSON, + encodeResultJSON, +} from "../../util/transaction.js"; + +const baseUrl = + "/devhub.near/widget/app?page=community&handle=webassemblymusic&tab=first-blog"; + +const communityAccount = "webassemblymusic.community.devhub.near"; + +// This blog is mocked in addons.js +const blogPage = + "/devhub.near/widget/app?page=blogv2&id=published-w5cj1y&community=webassemblymusic"; + +test.beforeEach(async ({ page }) => { + await page.route("http://localhost:20000/", async (route) => { + await mockDefaultTabs(route); + }); + + await page.route("https://api.near.social/get", async (route) => { + await mockBlogs(route); + }); +}); + +test.afterEach( + async ({ page }) => await page.unrouteAll({ behavior: "ignoreErrors" }) +); +test.describe("Don't ask again enabled", () => { + test.use({ + storageState: + "playwright-tests/storage-states/wallet-connected-with-devhub-access-key.json", + }); + + test.beforeEach(async ({ page }) => { + test.setTimeout(60000); + await page.goto(baseUrl); + + const widgetSrc = + "devhub.near/widget/devhub.entity.addon.blogv2.editor.provider"; + await setDontAskAgainCacheValues({ + page, + widgetSrc, + methodName: "set_community_socialdb", + contractId: "devhub.near", + }); + + await pauseIfVideoRecording(page); + }); + + test("Create a blog", async ({ page }) => { + // Start configuring the blog addon + const configureButton = page.getByTestId("configure-addon-button"); + await configureButton.click(); + + await pauseIfVideoRecording(page); + // Click new blog button + await page.getByTestId("new-blog-post-button").click(); + // Fill the title + await page.getByPlaceholder("Title", { exact: true }).click(); + const blogTitle = "the-blog-title"; + await page.getByPlaceholder("Title", { exact: true }).fill(blogTitle); + await page.getByPlaceholder("Title", { exact: true }).press("Tab"); + // Fill the subtitle + await page.getByPlaceholder("Subtitle").click(); + await page.getByPlaceholder("Subtitle").fill("Subtitle"); + // Choose the category + await page.getByPlaceholder("Description").scrollIntoViewIfNeeded(); + await page.getByText("News").first().click(); + await page.getByText("Guide", { exact: true }).click(); + // Fill the description + await page.getByPlaceholder("Description").click(); + const descriptionText = "A very specific description"; + await page.getByPlaceholder("Description").fill(descriptionText); + await page.getByPlaceholder("Description").press("Tab"); + // Fill the author + const author = "thomasguntenaar.near"; + await page.getByPlaceholder("Author").fill(author); + await page.getByPlaceholder("Author").press("Tab"); + // Fill the date + const publishedDate = "1998-05-03"; + await page.locator('input[name="date"]').fill("1998-05-03"); + await page.locator('input[name="date"]').press("Tab"); + // Fill the content + const content = "# Content"; + await page.frameLocator("iframe").getByRole("textbox").fill(content); + + // Mock transaction here + let is_transaction_completed = false; + await mockTransactionSubmitRPCResponses( + page, + async ({ route, request, transaction_completed, last_receiver_id }) => { + const requestPostData = request.postDataJSON(); + const args_base64 = requestPostData.params?.args_base64; + + if (transaction_completed) { + is_transaction_completed = true; + } + + if ( + // Make sure the addons are enabled + requestPostData.params && + requestPostData.params.account_id === "devhub.near" && + requestPostData.params.method_name === "get_community" + ) { + const response = await route.fetch(); + const json = await response.json(); + + const resultObj = decodeResultJSON(json.result.result); + if ( + !resultObj.addons + .map((addon) => addon.addon_id) + .includes("blogv2") || + !resultObj.addons + .map((addon) => addon.addon_id) + .includes("blogv2instance2") + ) { + resultObj.addons = [ + ...resultObj.addons, + { + addon_id: "blogv2", + display_name: "First Blog", + enabled: true, + id: "blogv2", + parameters: + '{"title":"Mocked configured blog page title",\ + "subtitle":"Mocked configured subtitle",\ + "authorEnabled": "enabled",\ + "searchEnabled": "enabled",\ + "orderBy": "timeasc",\ + "postPerPage": 5,\ + "categoriesEnabled": "enabled",\ + "categories": ["news", "guide", "reference"],\ + "categoryRequired": false}', + }, + { + addon_id: "blogv2", + display_name: "Second Blog", + enabled: true, + id: "blogv2instance2", + parameters: "{}", + }, + { + addon_id: "blogv2", + display_name: "Third Blog", + enabled: true, + id: "g1709r", + parameters: + '{"title": "WebAssemblyMusic",\ + "subtitle": "Stay up to date with the community blog",\ + "authorEnabled": "disabled",\ + "searchEnabled": "disabled",\ + "orderBy": "alpha",\ + "postPerPage": 10,\ + "categoriesEnabled": "enabled",\ + "categories": [{\ + "category": "News",\ + "value": "news"},\ + {\ + "category": "Olivier",\ + "value": "olivier"}],\ + "categoryRequired": "not_required"\ + }', + }, + ]; + } + + json.result.result = encodeResultJSON(resultObj); + + await route.fulfill({ response, json }); + return; + } + + await route.continue(); + } + ); + // Switch to publish instead of draft + await page.getByTestId("toggle-dropdown").click(); + + // Expect the indicator to not be visible + const loadingIndicator = page + .locator(".submit-blog-loading-indicator") + .first(); + + await expect(loadingIndicator).not.toBeVisible(); + + // Click on publish + await page + .getByTestId("submit-button-option-PUBLISH") + .locator("div") + .first() + .click(); + + await expect(loadingIndicator).toBeVisible(); + + // Expect the post button to be disabled + const parentDiv = page.getByTestId("parent-submit-blog-button"); + const parentDivClasses = + "select-header d-flex gap-1 align-items-center submit-draft-button disabled"; + await expect(parentDiv).toHaveClass(parentDivClasses); + + await expect(page.locator("div.modal-body code")).not.toBeVisible(); + + const transaction_toast = page.getByText( + "Calling contract devhub.near with method set_community_socialdb" + ); + await expect(transaction_toast).toBeVisible(); + + await pauseIfVideoRecording(page); + // Wait for the transaction to complete + await expect(transaction_toast).not.toBeVisible(); + await expect(loadingIndicator).not.toBeVisible(); + + await pauseIfVideoRecording(page); + expect(is_transaction_completed).toBe(true); + }); + + test("Update a blog", async ({ page }) => { + // test before each + + // Start configuring the blog addon + const configureButton = page.getByTestId("configure-addon-button"); + await configureButton.click(); + + await pauseIfVideoRecording(page); + + // Click a blog to edit + await page.getByRole("cell", { name: "PublishedBlog" }).click(); + // Change the category + await page.getByText("News", { exact: true }).click(); + await page.getByText("Reference").click(); + + // Mock transaction here + let is_transaction_completed = false; + await mockTransactionSubmitRPCResponses( + page, + async ({ route, request, transaction_completed, last_receiver_id }) => { + const requestPostData = request.postDataJSON(); + const args_base64 = requestPostData.params?.args_base64; + + if (transaction_completed && args_base64) { + // Check if the transaction is completed + is_transaction_completed = true; + } + // This is to intercept the asyncView call to get the blog data + // It's different from the Social.get call because they use different endpoints + if ( + requestPostData.params.account_id === "social.near" && + requestPostData.params.method_name === "get" && + args_base64 && + JSON.parse(atob(args_base64)).keys[0] === + `${communityAccount}/blog/**` + ) { + const response = await route.fetch(); + const json = await response.json(); + + const resultObj = decodeResultJSON(json.result.result); + + resultObj[communityAccount].blog[ + "published-w5cj1y" + ].metadata.category = "reference"; + + json.result.result = encodeResultJSON(resultObj); + + await route.fulfill({ response, json }); + return; + } else if ( + // Make sure the addons are enabled + requestPostData.params && + requestPostData.params.account_id === "devhub.near" && + requestPostData.params.method_name === "get_community" + ) { + const response = await route.fetch(); + const json = await response.json(); + + const resultObj = decodeResultJSON(json.result.result); + if ( + !resultObj.addons + .map((addon) => addon.addon_id) + .includes("blogv2") || + !resultObj.addons + .map((addon) => addon.addon_id) + .includes("blogv2instance2") + ) { + resultObj.addons = [ + ...resultObj.addons, + { + addon_id: "blogv2", + display_name: "First Blog", + enabled: true, + id: "blogv2", + parameters: "{categories:['news','guide','reference']}", + }, + { + addon_id: "blogv2", + display_name: "Second Blog", + enabled: true, + id: "blogv2instance2", + parameters: "{categories:['news','guide','reference']}", + }, + ]; + } + + json.result.result = encodeResultJSON(resultObj); + + await route.fulfill({ response, json }); + return; + } + + await route.continue(); + } + ); + + // Click post button + const postButton = page + .getByTestId("submit-blog-button") + .getByText("Publish"); + const loadingIndicator = await page + .locator(".submit-blog-loading-indicator") + .first(); + + await expect(loadingIndicator).not.toBeVisible(); + + // Show loading indicator + await postButton.click(); + + await expect(loadingIndicator).toBeVisible(); + + // Expect the post button to be disabled + const parentDiv = page.getByTestId("parent-submit-blog-button"); + const parentDivClasses = + "select-header d-flex gap-1 align-items-center submit-draft-button disabled"; + await expect(parentDiv).toHaveClass(parentDivClasses); + + await expect(page.locator("div.modal-body code")).not.toBeVisible(); + + const transaction_toast = page.getByText( + "Calling contract devhub.near with method set_community_socialdb" + ); + await expect(transaction_toast).toBeVisible(); + + // Wait for the transaction to complete + await expect(transaction_toast).not.toBeVisible(); + await expect(loadingIndicator).not.toBeVisible({ timeout: 10000 }); + + await pauseIfVideoRecording(page); + await expect(is_transaction_completed).toBe(true); + }); + + test("Delete a blog", async ({ page }) => { + // test before each + + // Start configuring the blog addon + const configureButton = page.getByTestId("configure-addon-button"); + await configureButton.click(); + + await pauseIfVideoRecording(page); + + // Click a blog to edit + const blogRowSelector = '[id^="edit-blog-selector-"]'; + await page.waitForSelector(blogRowSelector); + const row = page.locator(blogRowSelector).first(); + await row.click(); + + // Mock transaction here + let is_transaction_completed = false; + await mockTransactionSubmitRPCResponses( + page, + async ({ route, request, transaction_completed, last_receiver_id }) => { + const requestPostData = request.postDataJSON(); + const args_base64 = requestPostData.params?.args_base64; + + if (transaction_completed) { + is_transaction_completed = true; + } + + if ( + requestPostData.params.account_id === "social.near" && + requestPostData.params.method_name === "get" && + args_base64 && + JSON.parse(atob(args_base64)).keys[0] === + `${communityAccount}/blog/**` + ) { + const response = await route.fetch(); + const json = await response.json(); + + const resultObj = decodeResultJSON(json.result.result); + + // Mock the deletion of the first blog id + const blogId = Object.keys(resultObj[communityAccount].blog)[0]; + resultObj[communityAccount].blog[blogId] = null; + + json.result.result = encodeResultJSON(resultObj); + + await route.fulfill({ response, json }); + return; + } else if ( + // Make sure the addons are enabled + requestPostData.params && + requestPostData.params.account_id === "devhub.near" && + requestPostData.params.method_name === "get_community" + ) { + const response = await route.fetch(); + const json = await response.json(); + + const resultObj = decodeResultJSON(json.result.result); + if ( + !resultObj.addons + .map((addon) => addon.addon_id) + .includes("blogv2") || + !resultObj.addons + .map((addon) => addon.addon_id) + .includes("blogv2instance2") + ) { + resultObj.addons = [ + ...resultObj.addons, + { + addon_id: "blogv2", + display_name: "First Blog", + enabled: true, + id: "blogv2", + parameters: "{categories:['news','guide','reference']}", + }, + { + addon_id: "blogv2", + display_name: "Second Blog", + enabled: true, + id: "blogv2instance2", + parameters: "{categories:['news','guide','reference']}", + }, + ]; + } + + json.result.result = encodeResultJSON(resultObj); + + await route.fulfill({ response, json }); + return; + } else if ( + // Replace the remote components with local developed components + requestPostData.params && + requestPostData.params.account_id === "social.near" && + requestPostData.params.method_name === "get" + ) { + const social_get_key = JSON.parse( + atob(requestPostData.params.args_base64) + ).keys[0]; + + const response = await route.fetch({ + url: "http://localhost:20000/", + }); + const devComponents = ( + await fetch("http://localhost:3030").then((r) => r.json()) + ).components; + const json = await response.json(); + + // Replace component with local component + if (devComponents[social_get_key]) { + const social_get_key_parts = social_get_key.split("/"); + const devWidget = {}; + devWidget[social_get_key_parts[0]] = { widget: {} }; + devWidget[social_get_key_parts[0]].widget[social_get_key_parts[2]] = + devComponents[social_get_key].code; + json.result.result = Array.from( + new TextEncoder().encode(JSON.stringify(devWidget)) + ); + } + + await route.fulfill({ response, json }); + return; + } + + await route.continue(); + } + ); + + // Click delete button + const deleteButton = await page.getByTestId("delete-blog-button"); + await expect(deleteButton).toBeVisible(); + await deleteButton.scrollIntoViewIfNeeded(); + await pauseIfVideoRecording(page); + await deleteButton.click(); + + await page.getByRole("button", { name: "Ready to Delete" }).click(); + // Show loading indicator + const transaction_toast = page.getByText( + "Calling contract devhub.near with method set_community_socialdb" + ); + await expect(transaction_toast).toBeVisible(); + + await expect(deleteButton).toBeDisabled(); + + await pauseIfVideoRecording(page); + + await page.waitForSelector("button[data-testid='new-blog-post-button']", { + state: "visible", + }); + + expect(is_transaction_completed).toBe(true); + }); +}); diff --git a/playwright-tests/tests/blog/blogv2.admin.spec.js b/playwright-tests/tests/blog/blogv2.admin.spec.js new file mode 100644 index 000000000..74b47192b --- /dev/null +++ b/playwright-tests/tests/blog/blogv2.admin.spec.js @@ -0,0 +1,429 @@ +import { expect, test } from "@playwright/test"; +import { pauseIfVideoRecording } from "../../testUtils.js"; +import { mockDefaultTabs } from "../../util/addons.js"; +import { mockBlogs } from "../../util/blogs.js"; +const baseUrl = + "/devhub.near/widget/app?page=community&handle=webassemblymusic&tab=first-blog"; + +test.beforeEach(async ({ page }) => { + await page.route("http://localhost:20000/", async (route) => { + await mockDefaultTabs(route); + }); + + await page.route("https://api.near.social/get", async (route) => { + await mockBlogs(route); + }); +}); + +test.afterEach( + async ({ page }) => await page.unrouteAll({ behavior: "ignoreErrors" }) +); + +test.describe("Admin wallet is connected", () => { + test.use({ + storageState: "playwright-tests/storage-states/wallet-connected-peter.json", + }); + + test("should be able to configure the blogv2 addon", async ({ page }) => { + test.setTimeout(60000); + await page.goto(baseUrl); + + await pauseIfVideoRecording(page); + + const blogCardSelector = '[id^="blog-card-"]'; + await page.waitForSelector(blogCardSelector); + + const card = page.locator(blogCardSelector).first(); + + expect(await card.isVisible()).toBeTruthy(); + + const configureButton = page.getByTestId("configure-addon-button"); + await configureButton.click(); + + await page.getByTestId("new-blog-post-button").click(); + await page.getByRole("button", { name: "Cancel" }).click(); + + const blogRowSelector = '[id^="edit-blog-selector-"]'; + await page.waitForSelector(blogRowSelector); + + const row = page.locator(blogRowSelector).first(); + + await row.click(); + await page.getByPlaceholder("Title", { exact: true }).click(); + }); + + test("should have an empty form if select new blog, except author", async ({ + page, + }) => { + await page.goto(baseUrl); + const blogCardSelector = '[id^="blog-card-"]'; + + await page.waitForSelector(blogCardSelector, { + state: "visible", + }); + + const configureButton = page.getByTestId("configure-addon-button"); + await configureButton.click(); + + // Create a new blog + await page.getByTestId("new-blog-post-button").click(); + + await pauseIfVideoRecording(page); + + const formSelector = `[id^="blog-editor-form"]`; + await page.waitForSelector(formSelector, { + state: "visible", + }); + + const inputFieldSelectors = [ + 'input[name="title"]', + 'input[name="subtitle"]', + 'textarea[name="description"]', + ]; + for (const inputSelector of inputFieldSelectors) { + await page.waitForSelector(inputSelector, { + state: "visible", + }); + const inputElement = await page.$(inputSelector); + + const inputValue = await inputElement.evaluate( + (element) => element.value + ); + + expect(inputValue).toBe(""); + } + // Check default author + const authorInputSelector = 'input[name="author"]'; + const authorInputElement = await page.$(authorInputSelector); + const authorInputValue = await authorInputElement.evaluate( + (element) => element.value + ); + expect(authorInputValue).toBe("petersalomonsen.near"); + // Check publish date to be automatically today + const dateInputSelector = 'input[name="date"]'; + const dateInputElement = await page.$(dateInputSelector); + const dateInputValue = await dateInputElement.evaluate( + (element) => element.value + ); + const publishedAtDate = new Date(); + const year = publishedAtDate.getFullYear(); + const month = (publishedAtDate.getMonth() + 1).toString().padStart(2, "0"); + const day = publishedAtDate.getDate().toString().padStart(2, "0"); + const initialFormattedDate = year + "-" + month + "-" + day; + expect(dateInputValue).toBe(initialFormattedDate); + }); + + test("should load blogs in the sidebar for a given handle", async ({ + page, + }) => { + await page.goto(baseUrl); + await pauseIfVideoRecording(page); + + const configureButton = page.getByTestId("configure-addon-button"); + await configureButton.click(); + + await page.waitForSelector(`[id^="edit-blog-selector-"]`); + + const sidebarBlogSelectors = await page.$$(`[id^="edit-blog-selector-"]`); + + expect(sidebarBlogSelectors.length).toBeGreaterThanOrEqual(1); + }); + + test.describe("should be able to edit a blog", () => { + test.beforeEach(async ({ page }) => { + await page.goto(baseUrl); + await pauseIfVideoRecording(page); + + const configureButton = page.getByTestId("configure-addon-button"); + await configureButton.click(); + await page.waitForSelector(`[id^="edit-blog-selector-"]`, { + state: "visible", + }); + }); + + test("should be able toggle the admin UI off to see the BlogOverview", async ({ + page, + }) => { + const configureButton = page.getByTestId("configure-addon-button-x"); + await configureButton.click(); + await page.waitForSelector(`[id^="blog-card-"]`, { + state: "visible", + }); + }); + test("should be able to save blog as DRAFT", async ({ page }) => { + await page.getByTestId("new-blog-post-button").click(); + + await page.getByPlaceholder("Title", { exact: true }).click(); + await page.getByPlaceholder("Title", { exact: true }).fill("Title"); + await page.getByPlaceholder("Title", { exact: true }).press("Tab"); + await page.getByPlaceholder("Subtitle").click(); + await page.getByPlaceholder("Subtitle").fill("Subtitle"); + await page.getByText("News").first().click(); + await page.getByText("Guide", { exact: true }).click(); + await page.getByPlaceholder("Description").click(); + await page.getByPlaceholder("Description").fill("Description"); + await page.getByPlaceholder("Description").press("Tab"); + await page.getByPlaceholder("Author").fill("Author"); + await page.getByPlaceholder("Author").press("Tab"); + await page.locator('input[name="date"]').fill("1998-05-03"); + + await page.locator('input[name="date"]').press("Tab"); + await page.frameLocator("iframe").getByRole("textbox").fill("# Content"); + + const submitButton = page.getByTestId("submit-blog-button"); + const parentButton = page.getByTestId("parent-submit-blog-button"); + + await submitButton.scrollIntoViewIfNeeded(); + await pauseIfVideoRecording(page); + + await submitButton.click(); + await parentButton.click(); + + const transactionObj = JSON.parse( + await page.locator("div.modal-body code").innerText() + ); + + const blogId = Object.keys(transactionObj.data.blog)[0]; + expect(transactionObj.data.blog[blogId].metadata.title).toBe("Title"); + expect(transactionObj.data.blog[blogId].metadata.publishedAt).toBe( + "1998-05-03" + ); + expect(transactionObj.data.blog[blogId].metadata.createdAt).toBe( + new Date().toISOString().slice(0, 10) + ); + expect(transactionObj.data.blog[blogId].metadata.updatedAt).toBe( + new Date().toISOString().slice(0, 10) + ); + expect(transactionObj.data.blog[blogId].metadata.subtitle).toBe( + "Subtitle" + ); + expect(transactionObj.data.blog[blogId].metadata.description).toBe( + "Description" + ); + expect(transactionObj.data.blog[blogId].metadata.author).toBe("Author"); + expect(transactionObj.data.blog[blogId].metadata.communityAddonId).toBe( + "blogv2" + ); + expect(transactionObj.data.blog[blogId].metadata.status).toBe("DRAFT"); + expect(transactionObj.data.blog[blogId].metadata.category).toBe("guide"); + }); + + test("should not be able to save a blog if any field is missing", async ({ + page, + }) => { + await page.getByTestId("new-blog-post-button").click(); + const parentDivClasses = + "select-header d-flex gap-1 align-items-center submit-draft-button disabled"; + + const submitButton = page.getByTestId("submit-blog-button"); + const parentDiv = page.getByTestId("parent-submit-blog-button"); + await expect(parentDiv).toHaveClass(parentDivClasses); + + await page.getByPlaceholder("Title", { exact: true }).click(); + await page.getByPlaceholder("Title", { exact: true }).fill("Title"); + await page.getByPlaceholder("Title", { exact: true }).press("Tab"); + await expect(parentDiv).toHaveClass(parentDivClasses); + + await page.getByPlaceholder("Subtitle").click(); + await page.getByPlaceholder("Subtitle").fill("Subtitle"); + await expect(parentDiv).toHaveClass(parentDivClasses); + await page.getByText("News").first().click(); + await page.getByText("Guide", { exact: true }).click(); + + await page.getByPlaceholder("Description").click(); + await page.getByPlaceholder("Description").fill("Description"); + await page.getByPlaceholder("Description").press("Tab"); + await expect(parentDiv).toHaveClass(parentDivClasses); + + await page.getByPlaceholder("Author").fill("Author"); + await page.getByPlaceholder("Author").press("Tab"); + await expect(parentDiv).toHaveClass(parentDivClasses); + + await page.locator('input[name="date"]').fill("1998-05-03"); + await page.locator('input[name="date"]').press("Tab"); + await expect(parentDiv).toHaveClass(parentDivClasses); + + await page.frameLocator("iframe").getByRole("textbox").fill("# Content"); + + await expect(submitButton).toBeEnabled(); + + await page.getByText("Save Draft").click(); + }); + + test("should be able to cancel editing a blog", async ({ page }) => { + // Click on the first row this blog title is called "PublishedBlog" + await page.getByRole("cell", { name: "PublishedBlog" }).click(); + + await page.getByRole("button", { name: "Cancel" }).click(); + await page.waitForSelector(`[id^="edit-blog-selector-"]`, { + state: "visible", + }); + const configureButton = page.getByTestId("configure-addon-button-x"); + await configureButton.click(); + await page.waitForSelector(`[id^="blog-card-"]`, { + state: "visible", + }); + }); + test("should be able to cancel a new blog", async ({ page }) => { + await page.getByTestId("new-blog-post-button").click(); + await page.getByRole("button", { name: "Cancel" }).click(); + await page.waitForSelector(`[id^="edit-blog-selector-"]`, { + state: "visible", + }); + const configureButton = page.getByTestId("configure-addon-button-x"); + await configureButton.click(); + await page.waitForSelector(`[id^="blog-card-"]`, { + state: "visible", + }); + }); + + test("should be able to publish a blog", async ({ page }) => { + await page.getByTestId("new-blog-post-button").click(); + // Fill the title + await page.getByPlaceholder("Title", { exact: true }).click(); + await page.getByPlaceholder("Title", { exact: true }).fill("Title"); + await page.getByPlaceholder("Title", { exact: true }).press("Tab"); + // Fill the subtitle + await page.getByPlaceholder("Subtitle").click(); + await page.getByPlaceholder("Subtitle").fill("Subtitle"); + // Select News category + await page.getByText("News").first().click(); + await page.getByText("Guide", { exact: true }).click(); + await page.getByPlaceholder("Description").click(); + await page.getByPlaceholder("Description").fill("Description"); + await page.getByPlaceholder("Description").press("Tab"); + await page.getByPlaceholder("Author").fill("Author"); + await page.getByPlaceholder("Author").press("Tab"); + await page.locator('input[name="date"]').fill("1998-05-03"); + await page.locator('input[name="date"]').press("Tab"); + await page.frameLocator("iframe").getByRole("textbox").fill("# Content"); + + const publishToggle = page.getByTestId("toggle-dropdown"); + await publishToggle.scrollIntoViewIfNeeded(); + await pauseIfVideoRecording(page); + await publishToggle.click(); + + const publishOption = page.getByTestId("submit-button-option-PUBLISH"); + + await publishOption.click(); + + const transactionObj = JSON.parse( + await page.locator("div.modal-body code").innerText() + ); + const blogId = Object.keys(transactionObj.data.blog)[0]; + + expect(transactionObj.data.blog[blogId].metadata.status).toBe("PUBLISH"); + }); + }); + + test("should be able to delete a blog", async ({ page }) => { + test.setTimeout(60000); + await page.goto(baseUrl); + + const blogCardSelector = '[id^="blog-card-"]'; + await page.waitForSelector(blogCardSelector); + + const card = page.locator(blogCardSelector).first(); + + expect(await card.isVisible()).toBeTruthy(); + + const configureButton = page.getByTestId("configure-addon-button"); + await configureButton.click(); + + const blogRowSelector = '[id^="edit-blog-selector-"]'; + await page.waitForSelector(blogRowSelector); + + const row = page.locator(blogRowSelector).first(); + + await pauseIfVideoRecording(page); + await row.click(); + + const deleteButton = page.getByTestId("delete-blog-button"); + await deleteButton.scrollIntoViewIfNeeded(); + await pauseIfVideoRecording(page); + await deleteButton.click(); + + await page.getByRole("button", { name: "Ready to Delete" }).click(); + + const transactionObj = JSON.parse( + await page.locator("div.modal-body code").innerText() + ); + const blogId = Object.keys(transactionObj.data.blog)[0]; + + await expect(page.locator("div.modal-body code")).toHaveText( + JSON.stringify( + { + handle: "webassemblymusic", + data: { + blog: { + [blogId]: { + "": null, + metadata: { + title: null, + createdAt: null, + updatedAt: null, + publishedAt: null, + status: null, + subtitle: null, + description: null, + author: null, + id: null, + category: null, + communityAddonId: null, + }, + }, + }, + }, + }, + null, + 2 + ) + ); + }); + + test("should show 3 timestamps in the BlogOverview", async ({ page }) => { + await page.goto(baseUrl); + await pauseIfVideoRecording(page); + + const blogCardSelector = '[id^="blog-card-"]'; + await page.waitForSelector(blogCardSelector); + + const card = page.locator(blogCardSelector).first(); + + expect(await card.isVisible()).toBeTruthy(); + const configureButton = page.getByTestId("configure-addon-button"); + await configureButton.click(); + + const blogRowSelector = '[id^="edit-blog-selector-"]'; + await page.waitForSelector(blogRowSelector); + + const row = page.locator(blogRowSelector).first(); + + await row.click(); + const createdAtColumn = page.getByTestId("createdAt"); + const updatedAtColumn = page.getByTestId("updatedAt"); + const publishedAtColumn = page.getByTestId("publishedAt"); + + expect(createdAtColumn.isVisible()).toBeTruthy(); + expect(updatedAtColumn.isVisible()).toBeTruthy(); + expect(publishedAtColumn.isVisible()).toBeTruthy(); + }); + + test.skip("should show the settings", async ({ page }) => { + await page.goto(baseUrl); + const page1Promise = page.waitForEvent("popup"); + await page.getByRole("link", { name: "Settings" }).click(); + }); + + test.skip("should show the analytics", async ({ page }) => { + await page.goto(baseUrl); + const configureButton = page.getByTestId("configure-addon-button"); + await configureButton.click(); + await page.waitForSelector("#edit-blog-selector-published-w5cj1y", { + state: "visible", + }); + const page1Promise = page.waitForEvent("popup"); + await page.getByRole("link", { name: "Analytics" }).click(); + // Test if it opens a new tab to posthog url + }); +}); diff --git a/playwright-tests/tests/blog/blogv2.spec.js b/playwright-tests/tests/blog/blogv2.spec.js index 063a313ef..d04e65fe8 100644 --- a/playwright-tests/tests/blog/blogv2.spec.js +++ b/playwright-tests/tests/blog/blogv2.spec.js @@ -1,25 +1,11 @@ import { expect, test } from "@playwright/test"; -import { - pauseIfVideoRecording, - generateRandom6CharUUID, -} from "../../testUtils.js"; +import { pauseIfVideoRecording } from "../../testUtils.js"; import { mockDefaultTabs } from "../../util/addons.js"; import { mockBlogs } from "../../util/blogs.js"; -import { setDontAskAgainCacheValues } from "../../util/cache.js"; -import { - mockTransactionSubmitRPCResponses, - decodeResultJSON, - encodeResultJSON, -} from "../../util/transaction.js"; const baseUrl = "/devhub.near/widget/app?page=community&handle=webassemblymusic&tab=first-blog"; -const otherInstance = - "/devhub.near/widget/app?page=community&handle=webassemblymusic&tab=second-blog"; - -const communityAccount = "webassemblymusic.community.devhub.near"; - // This blog is mocked in addons.js const blogPage = "/devhub.near/widget/app?page=blogv2&id=published-w5cj1y&community=webassemblymusic"; @@ -122,884 +108,3 @@ test.describe("Wallet is not connected", () => { expect(categories.length).toBeGreaterThan(0); }); }); - -test.describe("Don't ask again enabled", () => { - test.use({ - storageState: - "playwright-tests/storage-states/wallet-connected-with-devhub-access-key.json", - }); - - test.beforeEach(async ({ page }) => { - test.setTimeout(60000); - await page.goto(baseUrl); - - const widgetSrc = - "devhub.near/widget/devhub.entity.addon.blogv2.editor.provider"; - await setDontAskAgainCacheValues({ - page, - widgetSrc, - methodName: "set_community_socialdb", - contractId: "devhub.near", - }); - - await pauseIfVideoRecording(page); - }); - - test("Create a blog", async ({ page }) => { - // Start configuring the blog addon - const configureButton = page.getByTestId("configure-addon-button"); - await configureButton.click(); - - await pauseIfVideoRecording(page); - // Click new blog button - await page.getByTestId("new-blog-post-button").click(); - // Fill the title - await page.getByPlaceholder("Title", { exact: true }).click(); - const blogTitle = "the-blog-title"; - await page.getByPlaceholder("Title", { exact: true }).fill(blogTitle); - await page.getByPlaceholder("Title", { exact: true }).press("Tab"); - // Fill the subtitle - await page.getByPlaceholder("Subtitle").click(); - await page.getByPlaceholder("Subtitle").fill("Subtitle"); - // Choose the category - await page.getByPlaceholder("Description").scrollIntoViewIfNeeded(); - await page.getByText("News").first().click(); - await page.getByText("Guide", { exact: true }).click(); - // Fill the description - await page.getByPlaceholder("Description").click(); - const descriptionText = "A very specific description"; - await page.getByPlaceholder("Description").fill(descriptionText); - await page.getByPlaceholder("Description").press("Tab"); - // Fill the author - const author = "thomasguntenaar.near"; - await page.getByPlaceholder("Author").fill(author); - await page.getByPlaceholder("Author").press("Tab"); - // Fill the date - const publishedDate = "1998-05-03"; - await page.locator('input[name="date"]').fill("1998-05-03"); - await page.locator('input[name="date"]').press("Tab"); - // Fill the content - const content = "# Content"; - await page.frameLocator("iframe").getByRole("textbox").fill(content); - - // Mock transaction here - let is_transaction_completed = false; - await mockTransactionSubmitRPCResponses( - page, - async ({ route, request, transaction_completed, last_receiver_id }) => { - const requestPostData = request.postDataJSON(); - const args_base64 = requestPostData.params?.args_base64; - - if (transaction_completed) { - is_transaction_completed = true; - } - - if ( - // Make sure the addons are enabled - requestPostData.params && - requestPostData.params.account_id === "devhub.near" && - requestPostData.params.method_name === "get_community" - ) { - const response = await route.fetch(); - const json = await response.json(); - - const resultObj = decodeResultJSON(json.result.result); - if ( - !resultObj.addons - .map((addon) => addon.addon_id) - .includes("blogv2") || - !resultObj.addons - .map((addon) => addon.addon_id) - .includes("blogv2instance2") - ) { - resultObj.addons = [ - ...resultObj.addons, - { - addon_id: "blogv2", - display_name: "First Blog", - enabled: true, - id: "blogv2", - parameters: - '{"title":"Mocked configured blog page title",\ - "subtitle":"Mocked configured subtitle",\ - "authorEnabled": "enabled",\ - "searchEnabled": "enabled",\ - "orderBy": "timeasc",\ - "postPerPage": 5,\ - "categoriesEnabled": "enabled",\ - "categories": ["news", "guide", "reference"],\ - "categoryRequired": false}', - }, - { - addon_id: "blogv2", - display_name: "Second Blog", - enabled: true, - id: "blogv2instance2", - parameters: "{}", - }, - { - addon_id: "blogv2", - display_name: "Third Blog", - enabled: true, - id: "g1709r", - parameters: - '{"title": "WebAssemblyMusic",\ - "subtitle": "Stay up to date with the community blog",\ - "authorEnabled": "disabled",\ - "searchEnabled": "disabled",\ - "orderBy": "alpha",\ - "postPerPage": 10,\ - "categoriesEnabled": "enabled",\ - "categories": [{\ - "category": "News",\ - "value": "news"},\ - {\ - "category": "Olivier",\ - "value": "olivier"}],\ - "categoryRequired": "not_required"\ - }', - }, - ]; - } - - json.result.result = encodeResultJSON(resultObj); - - await route.fulfill({ response, json }); - return; - } - - await route.continue(); - } - ); - // Switch to publish instead of draft - await page.getByTestId("toggle-dropdown").click(); - - // Expect the indicator to not be visible - const loadingIndicator = page - .locator(".submit-blog-loading-indicator") - .first(); - - await expect(loadingIndicator).not.toBeVisible(); - - // Click on publish - await page - .getByTestId("submit-button-option-PUBLISH") - .locator("div") - .first() - .click(); - - await expect(loadingIndicator).toBeVisible(); - - // Expect the post button to be disabled - const parentDiv = page.getByTestId("parent-submit-blog-button"); - const parentDivClasses = - "select-header d-flex gap-1 align-items-center submit-draft-button disabled"; - await expect(parentDiv).toHaveClass(parentDivClasses); - - await expect(page.locator("div.modal-body code")).not.toBeVisible(); - - const transaction_toast = page.getByText( - "Calling contract devhub.near with method set_community_socialdb" - ); - await expect(transaction_toast).toBeVisible(); - - await pauseIfVideoRecording(page); - // Wait for the transaction to complete - await expect(transaction_toast).not.toBeVisible(); - await expect(loadingIndicator).not.toBeVisible(); - - await pauseIfVideoRecording(page); - expect(is_transaction_completed).toBe(true); - }); - - test("Update a blog", async ({ page }) => { - // test before each - - // Start configuring the blog addon - const configureButton = page.getByTestId("configure-addon-button"); - await configureButton.click(); - - await pauseIfVideoRecording(page); - - // Click a blog to edit - await page.getByRole("cell", { name: "PublishedBlog" }).click(); - // Change the category - await page.getByText("News", { exact: true }).click(); - await page.getByText("Reference").click(); - - // Mock transaction here - let is_transaction_completed = false; - await mockTransactionSubmitRPCResponses( - page, - async ({ route, request, transaction_completed, last_receiver_id }) => { - const requestPostData = request.postDataJSON(); - const args_base64 = requestPostData.params?.args_base64; - - if (transaction_completed && args_base64) { - // Check if the transaction is completed - is_transaction_completed = true; - } - // This is to intercept the asyncView call to get the blog data - // It's different from the Social.get call because they use different endpoints - if ( - requestPostData.params.account_id === "social.near" && - requestPostData.params.method_name === "get" && - args_base64 && - JSON.parse(atob(args_base64)).keys[0] === - `${communityAccount}/blog/**` - ) { - const response = await route.fetch(); - const json = await response.json(); - - const resultObj = decodeResultJSON(json.result.result); - - resultObj[communityAccount].blog[ - "published-w5cj1y" - ].metadata.category = "reference"; - - json.result.result = encodeResultJSON(resultObj); - - await route.fulfill({ response, json }); - return; - } else if ( - // Make sure the addons are enabled - requestPostData.params && - requestPostData.params.account_id === "devhub.near" && - requestPostData.params.method_name === "get_community" - ) { - const response = await route.fetch(); - const json = await response.json(); - - const resultObj = decodeResultJSON(json.result.result); - if ( - !resultObj.addons - .map((addon) => addon.addon_id) - .includes("blogv2") || - !resultObj.addons - .map((addon) => addon.addon_id) - .includes("blogv2instance2") - ) { - resultObj.addons = [ - ...resultObj.addons, - { - addon_id: "blogv2", - display_name: "First Blog", - enabled: true, - id: "blogv2", - parameters: "{categories:['news','guide','reference']}", - }, - { - addon_id: "blogv2", - display_name: "Second Blog", - enabled: true, - id: "blogv2instance2", - parameters: "{categories:['news','guide','reference']}", - }, - ]; - } - - json.result.result = encodeResultJSON(resultObj); - - await route.fulfill({ response, json }); - return; - } - - await route.continue(); - } - ); - - // Click post button - const postButton = page - .getByTestId("submit-blog-button") - .getByText("Publish"); - const loadingIndicator = await page - .locator(".submit-blog-loading-indicator") - .first(); - - await expect(loadingIndicator).not.toBeVisible(); - - // Show loading indicator - await postButton.click(); - - await expect(loadingIndicator).toBeVisible(); - - // Expect the post button to be disabled - const parentDiv = page.getByTestId("parent-submit-blog-button"); - const parentDivClasses = - "select-header d-flex gap-1 align-items-center submit-draft-button disabled"; - await expect(parentDiv).toHaveClass(parentDivClasses); - - await expect(page.locator("div.modal-body code")).not.toBeVisible(); - - const transaction_toast = page.getByText( - "Calling contract devhub.near with method set_community_socialdb" - ); - await expect(transaction_toast).toBeVisible(); - - // Wait for the transaction to complete - await expect(transaction_toast).not.toBeVisible(); - await expect(loadingIndicator).not.toBeVisible({ timeout: 10000 }); - - await pauseIfVideoRecording(page); - await expect(is_transaction_completed).toBe(true); - }); - - test("Delete a blog", async ({ page }) => { - // test before each - - // Start configuring the blog addon - const configureButton = page.getByTestId("configure-addon-button"); - await configureButton.click(); - - await pauseIfVideoRecording(page); - - // Click a blog to edit - const blogRowSelector = '[id^="edit-blog-selector-"]'; - await page.waitForSelector(blogRowSelector); - const row = page.locator(blogRowSelector).first(); - await row.click(); - - // Mock transaction here - let is_transaction_completed = false; - await mockTransactionSubmitRPCResponses( - page, - async ({ route, request, transaction_completed, last_receiver_id }) => { - const requestPostData = request.postDataJSON(); - const args_base64 = requestPostData.params?.args_base64; - - if (transaction_completed) { - is_transaction_completed = true; - } - - if ( - requestPostData.params.account_id === "social.near" && - requestPostData.params.method_name === "get" && - args_base64 && - JSON.parse(atob(args_base64)).keys[0] === - `${communityAccount}/blog/**` - ) { - const response = await route.fetch(); - const json = await response.json(); - - const resultObj = decodeResultJSON(json.result.result); - - // Mock the deletion of the first blog id - const blogId = Object.keys(resultObj[communityAccount].blog)[0]; - resultObj[communityAccount].blog[blogId] = null; - - json.result.result = encodeResultJSON(resultObj); - - await route.fulfill({ response, json }); - return; - } else if ( - // Make sure the addons are enabled - requestPostData.params && - requestPostData.params.account_id === "devhub.near" && - requestPostData.params.method_name === "get_community" - ) { - const response = await route.fetch(); - const json = await response.json(); - - const resultObj = decodeResultJSON(json.result.result); - if ( - !resultObj.addons - .map((addon) => addon.addon_id) - .includes("blogv2") || - !resultObj.addons - .map((addon) => addon.addon_id) - .includes("blogv2instance2") - ) { - resultObj.addons = [ - ...resultObj.addons, - { - addon_id: "blogv2", - display_name: "First Blog", - enabled: true, - id: "blogv2", - parameters: "{categories:['news','guide','reference']}", - }, - { - addon_id: "blogv2", - display_name: "Second Blog", - enabled: true, - id: "blogv2instance2", - parameters: "{categories:['news','guide','reference']}", - }, - ]; - } - - json.result.result = encodeResultJSON(resultObj); - - await route.fulfill({ response, json }); - return; - } else if ( - // Replace the remote components with local developed components - requestPostData.params && - requestPostData.params.account_id === "social.near" && - requestPostData.params.method_name === "get" - ) { - const social_get_key = JSON.parse( - atob(requestPostData.params.args_base64) - ).keys[0]; - - const response = await route.fetch({ - url: "http://localhost:20000/", - }); - const devComponents = ( - await fetch("http://localhost:3030").then((r) => r.json()) - ).components; - const json = await response.json(); - - // Replace component with local component - if (devComponents[social_get_key]) { - const social_get_key_parts = social_get_key.split("/"); - const devWidget = {}; - devWidget[social_get_key_parts[0]] = { widget: {} }; - devWidget[social_get_key_parts[0]].widget[social_get_key_parts[2]] = - devComponents[social_get_key].code; - json.result.result = Array.from( - new TextEncoder().encode(JSON.stringify(devWidget)) - ); - } - - await route.fulfill({ response, json }); - return; - } - - await route.continue(); - } - ); - - // Click delete button - const deleteButton = page.getByTestId("delete-blog-button"); - await deleteButton.scrollIntoViewIfNeeded(); - await pauseIfVideoRecording(page); - await deleteButton.click(); - - await page.getByRole("button", { name: "Ready to Delete" }).click(); - // Show loading indicator - const transaction_toast = page.getByText( - "Calling contract devhub.near with method set_community_socialdb" - ); - await expect(transaction_toast).toBeVisible(); - - await expect(deleteButton).toBeDisabled(); - - await pauseIfVideoRecording(page); - - await page.waitForSelector("button[data-testid='new-blog-post-button']", { - state: "visible", - }); - - expect(is_transaction_completed).toBe(true); - }); -}); - -test.describe("Admin wallet is connected", () => { - test.use({ - storageState: "playwright-tests/storage-states/wallet-connected-peter.json", - }); - - test("should be able to configure the blogv2 addon", async ({ page }) => { - await page.goto(baseUrl); - - await pauseIfVideoRecording(page); - - const blogCardSelector = '[id^="blog-card-"]'; - await page.waitForSelector(blogCardSelector); - - const card = page.locator(blogCardSelector).first(); - - expect(await card.isVisible()).toBeTruthy(); - - const configureButton = page.getByTestId("configure-addon-button"); - await configureButton.click(); - - await page.getByTestId("new-blog-post-button").click(); - await page.getByRole("button", { name: "Cancel" }).click(); - - const blogRowSelector = '[id^="edit-blog-selector-"]'; - await page.waitForSelector(blogRowSelector); - - const row = page.locator(blogRowSelector).first(); - - await row.click(); - await page.getByPlaceholder("Title", { exact: true }).click(); - }); - - test("should have an empty form if select new blog, except author", async ({ - page, - }) => { - await page.goto(baseUrl); - const blogCardSelector = '[id^="blog-card-"]'; - - await page.waitForSelector(blogCardSelector, { - state: "visible", - }); - - const configureButton = page.getByTestId("configure-addon-button"); - await configureButton.click(); - - // Create a new blog - await page.getByTestId("new-blog-post-button").click(); - - await pauseIfVideoRecording(page); - - const formSelector = `[id^="blog-editor-form"]`; - await page.waitForSelector(formSelector, { - state: "visible", - }); - - const inputFieldSelectors = [ - 'input[name="title"]', - 'input[name="subtitle"]', - 'textarea[name="description"]', - ]; - for (const inputSelector of inputFieldSelectors) { - await page.waitForSelector(inputSelector, { - state: "visible", - }); - const inputElement = await page.$(inputSelector); - - const inputValue = await inputElement.evaluate( - (element) => element.value - ); - - expect(inputValue).toBe(""); - } - // Check default author - const authorInputSelector = 'input[name="author"]'; - const authorInputElement = await page.$(authorInputSelector); - const authorInputValue = await authorInputElement.evaluate( - (element) => element.value - ); - expect(authorInputValue).toBe("petersalomonsen.near"); - // Check publish date to be automatically today - const dateInputSelector = 'input[name="date"]'; - const dateInputElement = await page.$(dateInputSelector); - const dateInputValue = await dateInputElement.evaluate( - (element) => element.value - ); - const publishedAtDate = new Date(); - const year = publishedAtDate.getFullYear(); - const month = (publishedAtDate.getMonth() + 1).toString().padStart(2, "0"); - const day = publishedAtDate.getDate().toString().padStart(2, "0"); - const initialFormattedDate = year + "-" + month + "-" + day; - expect(dateInputValue).toBe(initialFormattedDate); - }); - - test("should load blogs in the sidebar for a given handle", async ({ - page, - }) => { - await page.goto(baseUrl); - await pauseIfVideoRecording(page); - - const configureButton = page.getByTestId("configure-addon-button"); - await configureButton.click(); - - await page.waitForSelector(`[id^="edit-blog-selector-"]`); - - const sidebarBlogSelectors = await page.$$(`[id^="edit-blog-selector-"]`); - - expect(sidebarBlogSelectors.length).toBeGreaterThanOrEqual(1); - }); - - test.describe("should be able to edit a blog", () => { - test.beforeEach(async ({ page }) => { - await page.goto(baseUrl); - await pauseIfVideoRecording(page); - - const configureButton = page.getByTestId("configure-addon-button"); - await configureButton.click(); - await page.waitForSelector(`[id^="edit-blog-selector-"]`, { - state: "visible", - }); - }); - - test("should be able toggle the admin UI off to see the BlogOverview", async ({ - page, - }) => { - const configureButton = page.getByTestId("configure-addon-button-x"); - await configureButton.click(); - await page.waitForSelector(`[id^="blog-card-"]`, { - state: "visible", - }); - }); - test("should be able to save blog as DRAFT", async ({ page }) => { - await page.getByTestId("new-blog-post-button").click(); - - await page.getByPlaceholder("Title", { exact: true }).click(); - await page.getByPlaceholder("Title", { exact: true }).fill("Title"); - await page.getByPlaceholder("Title", { exact: true }).press("Tab"); - await page.getByPlaceholder("Subtitle").click(); - await page.getByPlaceholder("Subtitle").fill("Subtitle"); - await page.getByText("News").first().click(); - await page.getByText("Guide", { exact: true }).click(); - await page.getByPlaceholder("Description").click(); - await page.getByPlaceholder("Description").fill("Description"); - await page.getByPlaceholder("Description").press("Tab"); - await page.getByPlaceholder("Author").fill("Author"); - await page.getByPlaceholder("Author").press("Tab"); - await page.locator('input[name="date"]').fill("1998-05-03"); - - await page.locator('input[name="date"]').press("Tab"); - await page.frameLocator("iframe").getByRole("textbox").fill("# Content"); - - const submitButton = page.getByTestId("submit-blog-button"); - const parentButton = page.getByTestId("parent-submit-blog-button"); - - await submitButton.scrollIntoViewIfNeeded(); - await pauseIfVideoRecording(page); - - await submitButton.click(); - await parentButton.click(); - - const transactionObj = JSON.parse( - await page.locator("div.modal-body code").innerText() - ); - - const blogId = Object.keys(transactionObj.data.blog)[0]; - expect(transactionObj.data.blog[blogId].metadata.title).toBe("Title"); - expect(transactionObj.data.blog[blogId].metadata.publishedAt).toBe( - "1998-05-03" - ); - expect(transactionObj.data.blog[blogId].metadata.createdAt).toBe( - new Date().toISOString().slice(0, 10) - ); - expect(transactionObj.data.blog[blogId].metadata.updatedAt).toBe( - new Date().toISOString().slice(0, 10) - ); - expect(transactionObj.data.blog[blogId].metadata.subtitle).toBe( - "Subtitle" - ); - expect(transactionObj.data.blog[blogId].metadata.description).toBe( - "Description" - ); - expect(transactionObj.data.blog[blogId].metadata.author).toBe("Author"); - expect(transactionObj.data.blog[blogId].metadata.communityAddonId).toBe( - "blogv2" - ); - expect(transactionObj.data.blog[blogId].metadata.status).toBe("DRAFT"); - expect(transactionObj.data.blog[blogId].metadata.category).toBe("guide"); - }); - - test("should not be able to save a blog if any field is missing", async ({ - page, - }) => { - await page.getByTestId("new-blog-post-button").click(); - const parentDivClasses = - "select-header d-flex gap-1 align-items-center submit-draft-button disabled"; - - const submitButton = page.getByTestId("submit-blog-button"); - const parentDiv = page.getByTestId("parent-submit-blog-button"); - await expect(parentDiv).toHaveClass(parentDivClasses); - - await page.getByPlaceholder("Title", { exact: true }).click(); - await page.getByPlaceholder("Title", { exact: true }).fill("Title"); - await page.getByPlaceholder("Title", { exact: true }).press("Tab"); - await expect(parentDiv).toHaveClass(parentDivClasses); - - await page.getByPlaceholder("Subtitle").click(); - await page.getByPlaceholder("Subtitle").fill("Subtitle"); - await expect(parentDiv).toHaveClass(parentDivClasses); - await page.getByText("News").first().click(); - await page.getByText("Guide", { exact: true }).click(); - - await page.getByPlaceholder("Description").click(); - await page.getByPlaceholder("Description").fill("Description"); - await page.getByPlaceholder("Description").press("Tab"); - await expect(parentDiv).toHaveClass(parentDivClasses); - - await page.getByPlaceholder("Author").fill("Author"); - await page.getByPlaceholder("Author").press("Tab"); - await expect(parentDiv).toHaveClass(parentDivClasses); - - await page.locator('input[name="date"]').fill("1998-05-03"); - await page.locator('input[name="date"]').press("Tab"); - await expect(parentDiv).toHaveClass(parentDivClasses); - - await page.frameLocator("iframe").getByRole("textbox").fill("# Content"); - - await expect(submitButton).toBeEnabled(); - - await page.getByText("Save Draft").click(); - }); - - test("should be able to cancel editing a blog", async ({ page }) => { - // Click on the first row this blog title is called "PublishedBlog" - await page.getByRole("cell", { name: "PublishedBlog" }).click(); - - await page.getByRole("button", { name: "Cancel" }).click(); - await page.waitForSelector(`[id^="edit-blog-selector-"]`, { - state: "visible", - }); - const configureButton = page.getByTestId("configure-addon-button-x"); - await configureButton.click(); - await page.waitForSelector(`[id^="blog-card-"]`, { - state: "visible", - }); - }); - test("should be able to cancel a new blog", async ({ page }) => { - await page.getByTestId("new-blog-post-button").click(); - await page.getByRole("button", { name: "Cancel" }).click(); - await page.waitForSelector(`[id^="edit-blog-selector-"]`, { - state: "visible", - }); - const configureButton = page.getByTestId("configure-addon-button-x"); - await configureButton.click(); - await page.waitForSelector(`[id^="blog-card-"]`, { - state: "visible", - }); - }); - - test("should be able to publish a blog", async ({ page }) => { - await page.getByTestId("new-blog-post-button").click(); - // Fill the title - await page.getByPlaceholder("Title", { exact: true }).click(); - await page.getByPlaceholder("Title", { exact: true }).fill("Title"); - await page.getByPlaceholder("Title", { exact: true }).press("Tab"); - // Fill the subtitle - await page.getByPlaceholder("Subtitle").click(); - await page.getByPlaceholder("Subtitle").fill("Subtitle"); - // Select News category - await page.getByText("News").first().click(); - await page.getByText("Guide", { exact: true }).click(); - await page.getByPlaceholder("Description").click(); - await page.getByPlaceholder("Description").fill("Description"); - await page.getByPlaceholder("Description").press("Tab"); - await page.getByPlaceholder("Author").fill("Author"); - await page.getByPlaceholder("Author").press("Tab"); - await page.locator('input[name="date"]').fill("1998-05-03"); - await page.locator('input[name="date"]').press("Tab"); - await page.frameLocator("iframe").getByRole("textbox").fill("# Content"); - - const publishToggle = page.getByTestId("toggle-dropdown"); - await publishToggle.scrollIntoViewIfNeeded(); - await pauseIfVideoRecording(page); - await publishToggle.click(); - - const publishOption = page.getByTestId("submit-button-option-PUBLISH"); - - await publishOption.click(); - - const transactionObj = JSON.parse( - await page.locator("div.modal-body code").innerText() - ); - const blogId = Object.keys(transactionObj.data.blog)[0]; - - expect(transactionObj.data.blog[blogId].metadata.status).toBe("PUBLISH"); - }); - }); - - test("should be able to delete a blog", async ({ page }) => { - await page.goto(baseUrl); - await pauseIfVideoRecording(page); - - const blogCardSelector = '[id^="blog-card-"]'; - await page.waitForSelector(blogCardSelector); - - const card = page.locator(blogCardSelector).first(); - - expect(await card.isVisible()).toBeTruthy(); - - const configureButton = page.getByTestId("configure-addon-button"); - await configureButton.click(); - - const blogRowSelector = '[id^="edit-blog-selector-"]'; - await page.waitForSelector(blogRowSelector); - - const row = page.locator(blogRowSelector).first(); - - await row.click(); - - await pauseIfVideoRecording(page); - - const deleteButton = page.getByTestId("delete-blog-button"); - await deleteButton.scrollIntoViewIfNeeded(); - await pauseIfVideoRecording(page); - await deleteButton.click(); - - await page.getByRole("button", { name: "Ready to Delete" }).click(); - - const transactionObj = JSON.parse( - await page.locator("div.modal-body code").innerText() - ); - const blogId = Object.keys(transactionObj.data.blog)[0]; - - await expect(page.locator("div.modal-body code")).toHaveText( - JSON.stringify( - { - handle: "webassemblymusic", - data: { - blog: { - [blogId]: { - "": null, - metadata: { - title: null, - createdAt: null, - updatedAt: null, - publishedAt: null, - status: null, - subtitle: null, - description: null, - author: null, - id: null, - category: null, - communityAddonId: null, - }, - }, - }, - }, - }, - null, - 2 - ) - ); - }); - - test("should show 3 timestamps in the BlogOverview", async ({ page }) => { - await page.goto(baseUrl); - await pauseIfVideoRecording(page); - - const blogCardSelector = '[id^="blog-card-"]'; - await page.waitForSelector(blogCardSelector); - - const card = page.locator(blogCardSelector).first(); - - expect(await card.isVisible()).toBeTruthy(); - const configureButton = page.getByTestId("configure-addon-button"); - await configureButton.click(); - - const blogRowSelector = '[id^="edit-blog-selector-"]'; - await page.waitForSelector(blogRowSelector); - - const row = page.locator(blogRowSelector).first(); - - await row.click(); - const createdAtColumn = page.getByTestId("createdAt"); - const updatedAtColumn = page.getByTestId("updatedAt"); - const publishedAtColumn = page.getByTestId("publishedAt"); - - expect(createdAtColumn.isVisible()).toBeTruthy(); - expect(updatedAtColumn.isVisible()).toBeTruthy(); - expect(publishedAtColumn.isVisible()).toBeTruthy(); - }); - - test.skip("should show the settings", async ({ page }) => { - await page.goto(baseUrl); - const page1Promise = page.waitForEvent("popup"); - await page.getByRole("link", { name: "Settings" }).click(); - }); - - test.skip("should show the analytics", async ({ page }) => { - await page.goto(baseUrl); - const configureButton = page.getByTestId("configure-addon-button"); - await configureButton.click(); - await page.waitForSelector("#edit-blog-selector-published-w5cj1y", { - state: "visible", - }); - const page1Promise = page.waitForEvent("popup"); - await page.getByRole("link", { name: "Analytics" }).click(); - // Test if it opens a new tab to posthog url - }); -}); From 75968ba5b91e1d2735fe1a3bc915be535578b539 Mon Sep 17 00:00:00 2001 From: Peter Salomonsen Date: Thu, 11 Jul 2024 12:25:03 +0000 Subject: [PATCH 7/9] fix delete blog, which deletes a specific blog id "published-w5cj1y2" remove redundant replacing of dev components. make sure that rpc and near social api returns the same blog content --- .../entity/addon/blogv2/editor/content.jsx | 7 +- .../blog/blogv2.admin.dontaskagain.spec.js | 51 +--- playwright-tests/util/addons.js | 36 --- playwright-tests/util/blogs.js | 244 +++++++++--------- 4 files changed, 134 insertions(+), 204 deletions(-) diff --git a/instances/devhub.near/widget/devhub/entity/addon/blogv2/editor/content.jsx b/instances/devhub.near/widget/devhub/entity/addon/blogv2/editor/content.jsx index 67721ef5e..caf0972a4 100644 --- a/instances/devhub.near/widget/devhub/entity/addon/blogv2/editor/content.jsx +++ b/instances/devhub.near/widget/devhub/entity/addon/blogv2/editor/content.jsx @@ -217,15 +217,12 @@ useEffect(() => { }, }).then((result) => { try { - if ( - JSON.parse(result[communityAccount].blog[submittedBlogDeleted]) === - null - ) { + if (!result[communityAccount].blog[submittedBlogDeleted]) { // Blog is deleted setSubmittedBlogDeleted(null); } } catch (e) {} - setTimeout(() => checkForDeletedBlogInSocialDB(), 1000); + setTimeout(() => checkForDeletedBlogInSocialDB(), 500); }); }; if (submittedBlogDeleted) { diff --git a/playwright-tests/tests/blog/blogv2.admin.dontaskagain.spec.js b/playwright-tests/tests/blog/blogv2.admin.dontaskagain.spec.js index 21eb54851..cef8b82ca 100644 --- a/playwright-tests/tests/blog/blogv2.admin.dontaskagain.spec.js +++ b/playwright-tests/tests/blog/blogv2.admin.dontaskagain.spec.js @@ -1,7 +1,7 @@ import { expect, test } from "@playwright/test"; import { pauseIfVideoRecording } from "../../testUtils.js"; import { mockDefaultTabs } from "../../util/addons.js"; -import { mockBlogs } from "../../util/blogs.js"; +import { mockBlogs, blogs } from "../../util/blogs.js"; import { setDontAskAgainCacheValues } from "../../util/cache.js"; import { mockTransactionSubmitRPCResponses, @@ -14,10 +14,6 @@ const baseUrl = const communityAccount = "webassemblymusic.community.devhub.near"; -// This blog is mocked in addons.js -const blogPage = - "/devhub.near/widget/app?page=blogv2&id=published-w5cj1y&community=webassemblymusic"; - test.beforeEach(async ({ page }) => { await page.route("http://localhost:20000/", async (route) => { await mockDefaultTabs(route); @@ -390,11 +386,13 @@ test.describe("Don't ask again enabled", () => { const json = await response.json(); const resultObj = decodeResultJSON(json.result.result); + resultObj[communityAccount].blog = blogs; - // Mock the deletion of the first blog id - const blogId = Object.keys(resultObj[communityAccount].blog)[0]; - resultObj[communityAccount].blog[blogId] = null; + const blogId = "published-w5cj1y2"; + if (is_transaction_completed) { + resultObj[communityAccount].blog[blogId] = null; + } json.result.result = encodeResultJSON(resultObj); await route.fulfill({ response, json }); @@ -438,38 +436,6 @@ test.describe("Don't ask again enabled", () => { json.result.result = encodeResultJSON(resultObj); - await route.fulfill({ response, json }); - return; - } else if ( - // Replace the remote components with local developed components - requestPostData.params && - requestPostData.params.account_id === "social.near" && - requestPostData.params.method_name === "get" - ) { - const social_get_key = JSON.parse( - atob(requestPostData.params.args_base64) - ).keys[0]; - - const response = await route.fetch({ - url: "http://localhost:20000/", - }); - const devComponents = ( - await fetch("http://localhost:3030").then((r) => r.json()) - ).components; - const json = await response.json(); - - // Replace component with local component - if (devComponents[social_get_key]) { - const social_get_key_parts = social_get_key.split("/"); - const devWidget = {}; - devWidget[social_get_key_parts[0]] = { widget: {} }; - devWidget[social_get_key_parts[0]].widget[social_get_key_parts[2]] = - devComponents[social_get_key].code; - json.result.result = Array.from( - new TextEncoder().encode(JSON.stringify(devWidget)) - ); - } - await route.fulfill({ response, json }); return; } @@ -480,18 +446,17 @@ test.describe("Don't ask again enabled", () => { // Click delete button const deleteButton = await page.getByTestId("delete-blog-button"); - await expect(deleteButton).toBeVisible(); + await expect(deleteButton).toBeAttached(); await deleteButton.scrollIntoViewIfNeeded(); await pauseIfVideoRecording(page); await deleteButton.click(); await page.getByRole("button", { name: "Ready to Delete" }).click(); // Show loading indicator - const transaction_toast = page.getByText( + const transaction_toast = await page.getByText( "Calling contract devhub.near with method set_community_socialdb" ); await expect(transaction_toast).toBeVisible(); - await expect(deleteButton).toBeDisabled(); await pauseIfVideoRecording(page); diff --git a/playwright-tests/util/addons.js b/playwright-tests/util/addons.js index e2f7a1894..21c1e54ff 100644 --- a/playwright-tests/util/addons.js +++ b/playwright-tests/util/addons.js @@ -4,10 +4,6 @@ export async function mockDefaultTabs(route) { const request = await route.request(); const requestPostData = request.postDataJSON(); - const devComponents = ( - await fetch("http://localhost:3030").then((r) => r.json()) - ).components; - if ( requestPostData.params && requestPostData.params.account_id === "devhub.near" && @@ -121,38 +117,6 @@ export async function mockDefaultTabs(route) { await route.fulfill({ response, json }); return; - } else if ( - requestPostData.params && - requestPostData.params.account_id === "social.near" && - requestPostData.params.method_name === "get" - ) { - const social_get_key = JSON.parse(atob(requestPostData.params.args_base64)) - .keys[0]; - - const response = await route.fetch({ - url: "http://localhost:20000/", - }); - let json = {}; - try { - json = await response.json(); - } catch (error) { - console.error("Error parsing JSON response"); - console.log(JSON.stringify(await response.text())); - } - - // Replace component with local component - if (devComponents[social_get_key]) { - const social_get_key_parts = social_get_key.split("/"); - const devWidget = {}; - devWidget[social_get_key_parts[0]] = { widget: {} }; - devWidget[social_get_key_parts[0]].widget[social_get_key_parts[2]] = - devComponents[social_get_key].code; - json.result.result = Array.from( - new TextEncoder().encode(JSON.stringify(devWidget)) - ); - } - - await route.fulfill({ response, json }); } else { await route.continue(); } diff --git a/playwright-tests/util/blogs.js b/playwright-tests/util/blogs.js index 77fe298a7..a062b0e8a 100644 --- a/playwright-tests/util/blogs.js +++ b/playwright-tests/util/blogs.js @@ -1,5 +1,128 @@ const getRandomElement = (arr) => arr[Math.floor(Math.random() * arr.length)]; +export const blogs = { + "hello-world-0r4rmr": { + "": "# Content\n\n## subcontent\n\n### h3", + metadata: { + title: "Hello World", + createdAt: "2024-04-28", + updatedAt: "2024-04-28", + publishedAt: "1998-05-03", + status: "DRAFT", + subtitle: "Subtitle", + description: "Description", + author: "thomasguntenaar.near", + communityAddonId: "blogv2", + category: "guide", + }, + }, + "published-w5cj1y": { + "": "# Content\n\n", + metadata: { + title: "PublishedBlog", + createdAt: "2024-04-29", + updatedAt: "2024-04-29", + publishedAt: "2024-04-30", + status: "PUBLISH", + subtitle: "subtitle", + description: "Description", + author: "thomasguntenaar.near", + communityAddonId: "g1709r", + category: "news", + }, + }, + "published-w5cj1y2": { + "": "# Content\n\n", + metadata: { + title: "PublishedBlog", + createdAt: "2024-04-29", + updatedAt: "2024-04-29", + publishedAt: "2024-04-30", + status: "PUBLISH", + subtitle: "subtitle", + description: "Description", + author: "thomasguntenaar.near", + communityAddonId: "blogv2", + category: "news", + }, + }, + "this-is-the-blog-title-xfxkzh": { + "": "# Content\n\n", + metadata: { + title: "PublishedBlog", + createdAt: "2024-04-29", + updatedAt: "2024-04-29", + publishedAt: "2024-04-30", + status: "PUBLISH", + subtitle: "subtitle", + description: "Description", + author: "thomasguntenaar.near", + communityAddonId: "g1709r", + category: "news", + }, + }, + "first-blog-of-instance-2-nhasab": { + "": "# First ever blog seperate from instance 1", + metadata: { + title: "First blog of instance", + createdAt: "2024-04-30", + updatedAt: "2024-05-13", + publishedAt: "2024-04-30", + status: "PUBLISH", + subtitle: "Subtitle", + description: "Description", + author: "thomasguntenaar.near", + communityAddonId: "blogv2instance2", + category: "reference", + }, + }, + "new-blog-post-cgomff": { + "": "# Content", + metadata: { + title: "New Blog Post", + createdAt: "2024-05-01", + updatedAt: "2024-05-13", + publishedAt: "1998-05-03", + status: "PUBLISH", + subtitle: "Subtitle", + description: "Description", + author: "thomasguntenaar.near", + communityAddonId: "blogv2instance2", + category: "news", + }, + }, + "test-subscribe-mujrt8": { + "": "# Content", + metadata: { + title: "Test Subscribe", + publishedAt: "2023-04-03", + status: "PUBLISH", + subtitle: "subtitle", + description: "description", + author: "thomasguntenaar.near", + createdAt: "2024-05-01", + communityAddonId: "blogv2", + category: "guide", + updatedAt: "2024-05-13", + }, + }, + "fourth-instance-blog": { + "": "# Content", + metadata: { + title: "Fourth", + publishedAt: "2023-04-03", + status: "PUBLISH", + subtitle: "subtitle", + description: "description", + author: "thomasguntenaar.near", + createdAt: "2024-05-01", + communityAddonId: "blogv2instance4", + category: "news", + updatedAt: "2024-05-13", + }, + }, +}; + export function createLotsOfBlogs({ communityAddonIds }) { const topics = [ "Cows", @@ -67,126 +190,7 @@ export async function mockBlogs(route) { // Mock blog responses json[communityAccount]["blog"] = { - "hello-world-0r4rmr": { - "": "# Content\n\n## subcontent\n\n### h3", - metadata: { - title: "Hello World", - createdAt: "2024-04-28", - updatedAt: "2024-04-28", - publishedAt: "1998-05-03", - status: "DRAFT", - subtitle: "Subtitle", - description: "Description", - author: "thomasguntenaar.near", - communityAddonId: "blogv2", - category: "guide", - }, - }, - "published-w5cj1y": { - "": "# Content\n\n", - metadata: { - title: "PublishedBlog", - createdAt: "2024-04-29", - updatedAt: "2024-04-29", - publishedAt: "2024-04-30", - status: "PUBLISH", - subtitle: "subtitle", - description: "Description", - author: "thomasguntenaar.near", - communityAddonId: "g1709r", - category: "news", - }, - }, - "published-w5cj1y2": { - "": "# Content\n\n", - metadata: { - title: "PublishedBlog", - createdAt: "2024-04-29", - updatedAt: "2024-04-29", - publishedAt: "2024-04-30", - status: "PUBLISH", - subtitle: "subtitle", - description: "Description", - author: "thomasguntenaar.near", - communityAddonId: "blogv2", - category: "news", - }, - }, - "this-is-the-blog-title-xfxkzh": { - "": "# Content\n\n", - metadata: { - title: "PublishedBlog", - createdAt: "2024-04-29", - updatedAt: "2024-04-29", - publishedAt: "2024-04-30", - status: "PUBLISH", - subtitle: "subtitle", - description: "Description", - author: "thomasguntenaar.near", - communityAddonId: "g1709r", - category: "news", - }, - }, - "first-blog-of-instance-2-nhasab": { - "": "# First ever blog seperate from instance 1", - metadata: { - title: "First blog of instance", - createdAt: "2024-04-30", - updatedAt: "2024-05-13", - publishedAt: "2024-04-30", - status: "PUBLISH", - subtitle: "Subtitle", - description: "Description", - author: "thomasguntenaar.near", - communityAddonId: "blogv2instance2", - category: "reference", - }, - }, - "new-blog-post-cgomff": { - "": "# Content", - metadata: { - title: "New Blog Post", - createdAt: "2024-05-01", - updatedAt: "2024-05-13", - publishedAt: "1998-05-03", - status: "PUBLISH", - subtitle: "Subtitle", - description: "Description", - author: "thomasguntenaar.near", - communityAddonId: "blogv2instance2", - category: "news", - }, - }, - "test-subscribe-mujrt8": { - "": "# Content", - metadata: { - title: "Test Subscribe", - publishedAt: "2023-04-03", - status: "PUBLISH", - subtitle: "subtitle", - description: "description", - author: "thomasguntenaar.near", - createdAt: "2024-05-01", - communityAddonId: "blogv2", - category: "guide", - updatedAt: "2024-05-13", - }, - }, - "fourth-instance-blog": { - "": "# Content", - metadata: { - title: "Fourth", - publishedAt: "2023-04-03", - status: "PUBLISH", - subtitle: "subtitle", - description: "description", - author: "thomasguntenaar.near", - createdAt: "2024-05-01", - communityAddonId: "blogv2instance4", - category: "news", - updatedAt: "2024-05-13", - }, - }, + ...blogs, ...blogPosts, }; From 0cc577550ff2056ccb30127175ad16b0dfd26db7 Mon Sep 17 00:00:00 2001 From: Peter Salomonsen Date: Thu, 11 Jul 2024 12:43:14 +0000 Subject: [PATCH 8/9] added missing waiting for elements to be attached --- playwright-tests/tests/blog/blogv2.admin.spec.js | 3 ++- playwright-tests/tests/blog/blogv2.settings.spec.js | 11 ++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/playwright-tests/tests/blog/blogv2.admin.spec.js b/playwright-tests/tests/blog/blogv2.admin.spec.js index 74b47192b..36b12aff8 100644 --- a/playwright-tests/tests/blog/blogv2.admin.spec.js +++ b/playwright-tests/tests/blog/blogv2.admin.spec.js @@ -338,7 +338,8 @@ test.describe("Admin wallet is connected", () => { await pauseIfVideoRecording(page); await row.click(); - const deleteButton = page.getByTestId("delete-blog-button"); + const deleteButton = await page.getByTestId("delete-blog-button"); + await expect(deleteButton).toBeAttached(); await deleteButton.scrollIntoViewIfNeeded(); await pauseIfVideoRecording(page); await deleteButton.click(); diff --git a/playwright-tests/tests/blog/blogv2.settings.spec.js b/playwright-tests/tests/blog/blogv2.settings.spec.js index 2201d9314..04a1d5ade 100644 --- a/playwright-tests/tests/blog/blogv2.settings.spec.js +++ b/playwright-tests/tests/blog/blogv2.settings.spec.js @@ -197,7 +197,10 @@ test.describe("Don't ask again enabled", () => { } ); - const saveSettingsButton = page.getByTestId("save-settings-button").nth(1); + const saveSettingsButton = await page + .getByTestId("save-settings-button") + .nth(1); + await expect(saveSettingsButton).toBeAttached(); // Save the settings await saveSettingsButton.click(); @@ -325,7 +328,8 @@ test.describe("Admin wallet is connected", () => { const configureButton = page.getByTestId("configure-addon-button-x"); await configureButton.click(); - const title = page.getByTestId("blog-instance-title"); + const title = await page.getByTestId("blog-instance-title"); + await expect(title).toBeAttached(); await title.scrollIntoViewIfNeeded(); expect(await title.innerText()).toBe("Mocked configured blog page title"); @@ -346,7 +350,8 @@ test.describe("Admin wallet is connected", () => { const configureButton = page.getByTestId("configure-addon-button-x"); await configureButton.click(); - const subTitle = page.getByTestId("blog-instance-subtitle"); + const subTitle = await page.getByTestId("blog-instance-subtitle"); + await expect(subTitle).toBeAttached(); await subTitle.scrollIntoViewIfNeeded(); expect(await subTitle.innerText()).toBe("Mocked configured subtitle"); // Go to the other Viewer where it is not configured and see the default title From 2de9df45c04917172d69a8d860859fc7e2ceca05 Mon Sep 17 00:00:00 2001 From: Peter Salomonsen Date: Thu, 11 Jul 2024 12:56:29 +0000 Subject: [PATCH 9/9] wait for save settings button. half workers of cpu cores. --- playwright-tests/tests/blog/blogv2.settings.spec.js | 4 +++- playwright.config.js | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/playwright-tests/tests/blog/blogv2.settings.spec.js b/playwright-tests/tests/blog/blogv2.settings.spec.js index 04a1d5ade..154a2b396 100644 --- a/playwright-tests/tests/blog/blogv2.settings.spec.js +++ b/playwright-tests/tests/blog/blogv2.settings.spec.js @@ -249,7 +249,9 @@ test.describe("Admin wallet is connected", () => { expect(await settingsButton.isVisible()).toBe(true); await settingsButton.click(); - const saveSettingsButton = page.getByTestId("save-settings-button").first(); + const saveSettingsButton = await page + .getByTestId("save-settings-button") + .first(); await saveSettingsButton.scrollIntoViewIfNeeded(); await expect(saveSettingsButton).toBeVisible(); }); diff --git a/playwright.config.js b/playwright.config.js index 6627b7154..a872d00b3 100644 --- a/playwright.config.js +++ b/playwright.config.js @@ -28,7 +28,7 @@ export default defineConfig({ /* Retry on CI only */ retries: process.env.CI ? 8 : 0, /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? "100%" : undefined, + workers: process.env.CI ? "50%" : undefined, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: "line", /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */