diff --git a/.github/workflows/changesets.yml b/.github/workflows/changesets.yml index 3b071e66a..b52230e20 100644 --- a/.github/workflows/changesets.yml +++ b/.github/workflows/changesets.yml @@ -18,7 +18,7 @@ jobs: - name: setup node.js uses: actions/setup-node@v3 with: - node-version: 20 + node-version: 20.13.1 - name: install pnpm run: npm i pnpm@latest -g - name: setup npmrc diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f6df4cb07..371eaea18 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -17,7 +17,7 @@ jobs: - name: Install Node.js uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 20.13.1 - name: Install pnpm uses: pnpm/action-setup@v3 diff --git a/.nvmrc b/.nvmrc index b1855cbb8..67d2ffed5 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -v20.6.0 \ No newline at end of file +v20.13.1 \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 99b27114b..07ce69ef6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,7 +4,7 @@ # If you need more help, visit the Dockerfile reference guide at # https://docs.docker.com/go/dockerfile-reference/ -ARG NODE_VERSION=20.6.0 +ARG NODE_VERSION=20.13.1 ARG PACKAGE ARG PORT=3000 diff --git a/core/.github/workflows/playwright.yml b/core/.github/workflows/playwright.yml new file mode 100644 index 000000000..f314305dc --- /dev/null +++ b/core/.github/workflows/playwright.yml @@ -0,0 +1,27 @@ +name: Playwright Tests +on: + push: + branches: [main, master] + pull_request: + branches: [main, master] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install dependencies + run: npm install -g pnpm && pnpm install + - name: Install Playwright Browsers + run: pnpm exec playwright install --with-deps + - name: Run Playwright tests + run: pnpm exec playwright test + - uses: actions/upload-artifact@v4 + if: always() + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/core/.gitignore b/core/.gitignore new file mode 100644 index 000000000..cfaa9a41b --- /dev/null +++ b/core/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ + +playwright/.auth diff --git a/core/app/c/[communitySlug]/stages/components/Assign.tsx b/core/app/c/[communitySlug]/stages/components/Assign.tsx index 14603dc38..0dd78eeff 100644 --- a/core/app/c/[communitySlug]/stages/components/Assign.tsx +++ b/core/app/c/[communitySlug]/stages/components/Assign.tsx @@ -107,6 +107,7 @@ export default function Assign(props: Props) { size="sm" variant="outline" role="combobox" + name="Assign" aria-expanded={open} className="w-[150px] justify-between" > diff --git a/core/package.json b/core/package.json index c68170ced..b174c6c84 100644 --- a/core/package.json +++ b/core/package.json @@ -3,6 +3,8 @@ "version": "0.0.0", "private": true, "scripts": { + "playwright:test": "playwright test", + "playwright:ui": "playwright test --ui", "dev": "next dev -p 3000 --turbo | pino-pretty", "build": "SKIP_VALIDATION=true next build", "invite-users": "dotenv -e .env.local -e .env.development tsx scripts/invite.ts", @@ -81,6 +83,7 @@ "zod": "^3.22.4" }, "devDependencies": { + "@playwright/test": "^1.43.1", "@preconstruct/next": "^4.0.0", "@pubpub/eslint-config": "workspace:*", "@pubpub/prettier-config": "workspace:*", diff --git a/core/playwright.config.ts b/core/playwright.config.ts new file mode 100644 index 000000000..676f0bbc2 --- /dev/null +++ b/core/playwright.config.ts @@ -0,0 +1,77 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// require('dotenv').config(); + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: "./playwright", + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: "html", + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: "http://localhost:3000", + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { ...devices["Desktop Chrome"] }, + }, + + { + name: "firefox", + use: { ...devices["Desktop Firefox"] }, + }, + /* Test against WebKit on macOS is failing for me */ + // { + // name: "webkit", + // use: { ...devices["Desktop Safari"] }, + // }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: "pnpm dev", + // url: "http://127.0.0.1:3000", + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/core/playwright/assign.spec.ts b/core/playwright/assign.spec.ts new file mode 100644 index 000000000..3257fce05 --- /dev/null +++ b/core/playwright/assign.spec.ts @@ -0,0 +1,16 @@ +import { expect, test } from "@playwright/test"; + +const authFile = "playwright/.auth/user.json"; + +test("Assigning members to a pub", async ({ page }) => { + await page.goto("/login"); + await page.getByLabel("email").fill("all@pubpub.org"); + await page.getByRole("textbox", { name: "password" }).fill("pubpub-all"); + await page.getByRole("button", { name: "Sign in" }).click(); + + await page.waitForURL("/c/unjournal/stages"); + await page.getByRole("combobox").click(); + await page.getByRole("option", { name: "Jill Admin" }).click(); + await expect(page.getByText("Success", { exact: true })).toBeVisible(); + await page.context().storageState({ path: authFile }); +}); diff --git a/core/playwright/login.spec.ts b/core/playwright/login.spec.ts new file mode 100644 index 000000000..d9ee52526 --- /dev/null +++ b/core/playwright/login.spec.ts @@ -0,0 +1,26 @@ +import { expect, test } from "@playwright/test"; + +const authFile = "playwright/.auth/user.json"; + +test("Login", async ({ page }) => { + await page.goto("/login"); + await page.getByLabel("email").fill("all@pubpub.org"); + await page.getByRole("textbox", { name: "password" }).fill("pubpub-all"); + await page.getByRole("button", { name: "Sign in" }).click(); + await page.waitForURL("/c/unjournal/stages"); + await expect(page.getByRole("link", { name: "Stages" })).toBeVisible(); + await page.context().storageState({ path: authFile }); +}); + +test("Logout", async ({ page }) => { + // should replace login with startup and and eventual db actions with teardown steps + await page.goto("/login"); + await page.getByLabel("email").fill("all@pubpub.org"); + await page.getByRole("textbox", { name: "password" }).fill("pubpub-all"); + await page.getByRole("button", { name: "Sign in" }).click(); + await page.waitForURL("/c/unjournal/stages"); + await expect(page.getByRole("button", { name: "Logout" })).toBeVisible(); + await page.getByRole("button", { name: "Logout" }).click(); + await page.waitForURL("/login"); + await page.context().storageState({ path: authFile }); +}); diff --git a/core/prisma/exampleCommunitySeeds/croccroc.ts b/core/prisma/exampleCommunitySeeds/croccroc.ts index 16cd683fc..88d5cc46a 100644 --- a/core/prisma/exampleCommunitySeeds/croccroc.ts +++ b/core/prisma/exampleCommunitySeeds/croccroc.ts @@ -1,6 +1,4 @@ import { faker } from "@faker-js/faker"; -import { PubType } from "@prisma/client"; -import { v4 as uuidv4, v4 } from "uuid"; import { logger } from "logger"; @@ -8,15 +6,11 @@ import type { CommunitiesId } from "~/kysely/types/public/Communities"; import type { PubTypesId } from "~/kysely/types/public/PubTypes"; import { registerCorePubField } from "~/actions/_lib/init"; import { corePubFields } from "~/actions/corePubFields"; -// import { PrismaClient } from "@prisma/client"; -import { type db as kyselyDb } from "~/kysely/database"; -import { env } from "../../lib/env/env.mjs"; - -// import { FileUpload } from "../../lib/fields/fileUpload"; +import { db } from "~/kysely/database"; export const crocCrocId = "758ba348-92c7-46ec-9612-7afda81e2d70" as CommunitiesId; -export default async function main(db: typeof kyselyDb, communityUUID: CommunitiesId) { +export default async function main(communityUUID: CommunitiesId) { logger.info("Registering core fields"); for (const corePubField of corePubFields) { logger.info(`Registering core field ${corePubField.slug}`); diff --git a/core/prisma/exampleCommunitySeeds/unjournal.ts b/core/prisma/exampleCommunitySeeds/unjournal.ts index 99f31652f..98770f1a1 100644 --- a/core/prisma/exampleCommunitySeeds/unjournal.ts +++ b/core/prisma/exampleCommunitySeeds/unjournal.ts @@ -2,6 +2,11 @@ import { faker } from "@faker-js/faker"; import { PrismaClient } from "@prisma/client"; import { v4 as uuidv4 } from "uuid"; +import type { CommunitiesId } from "~/kysely/types/public/Communities"; +import type { PubTypesId } from "~/kysely/types/public/PubTypes"; +import { corePubFields } from "~/actions/corePubFields"; +import { db } from "~/kysely/database"; +import { StagesId } from "~/kysely/types/public/Stages"; import { env } from "../../lib/env/env.mjs"; import { FileUpload } from "../../lib/fields/fileUpload"; @@ -627,7 +632,45 @@ export default async function main(prisma: PrismaClient, communityUUID: string) }, }, }); - + const corePubSlugs = corePubFields.map((field) => field.slug); + const persistedCorePubFields = await db + .selectFrom("pub_fields") + .selectAll() + .where("pub_fields.slug", "in", corePubSlugs) + .execute(); + await db + .with("new_pubs", (db) => + db + .insertInto("pubs") + .values({ + community_id: communityUUID as CommunitiesId, + pub_type_id: submissionTypeId as PubTypesId, + }) + .returning("id") + ) + .with("pubs_in_stages", (db) => + db.insertInto("PubsInStages").values((eb) => [ + { + pubId: eb.selectFrom("new_pubs").select("id"), + stageId: stageIds[3] as StagesId, + }, + ]) + ) + .insertInto("pub_values") + .values((eb) => [ + { + pub_id: eb.selectFrom("new_pubs").select("new_pubs.id"), + field_id: persistedCorePubFields.find((field) => field.slug === "pubpub:title")!.id, + value: '"It Aint Ease Bein Cheese"', + }, + { + pub_id: eb.selectFrom("new_pubs").select("new_pubs.id"), + field_id: persistedCorePubFields.find((field) => field.slug === "pubpub:content")! + .id, + value: '"# Abstract"', + }, + ]) + .execute(); // await prisma.pub.update({ // where: { id: submission.id }, // data: { @@ -776,32 +819,4 @@ export default async function main(prisma: PrismaClient, communityUUID: string) }); }) ); - - // const pubIds = [...Array(7)].map((x) => uuidv4()); - // const submissionToEvaluate = await prisma.pub.create({ - // data: { - // pubTypeId: submissionTypeId, - // communityId: communityUUID, - // stages: { connect: { id: stageIds[3] } }, - - // values: { - // createMany: { - // data: [ - // { - // fieldId: fieldIds[0], - // value: "When Celebrities Speak: A Nationwide Twitter Experiment Promoting Vaccination In Indonesia", - // }, - // { - // fieldId: fieldIds[1], - // value: "Celebrity endorsements are often sought to influence public opinion. We ask whether celebrity endorsement per se has an effect beyond the fact that their statements are seen by many, and whether on net their statements actually lead people to change their beliefs. To do so, we conducted a nationwide Twitter experiment in Indonesia with 46 high-profile celebrities and organizations, with a total of 7.8 million followers, who agreed to let us randomly tweet or retweet content promoting immunization from their accounts. Our design exploits the structure of what information is passed on along a retweet chain on Twitter to parse reach versus endorsement effects. Endorsements matter: tweets that users can identify as being originated by a celebrity are far more likely to be liked or retweeted by users than similar tweets seen by the same users but without the celebrities' imprimatur. By contrast, explicitly citing sources in the tweets actually reduces diffusion. By randomizing which celebrities tweeted when, we find suggestive evidence that overall exposure to the campaign may influence beliefs about vaccination and knowledge of immunization-seeking behavior by one's network. Taken together, the findings suggest an important role for celebrity endorsement.", - // }, - // { - // fieldId: fieldIds[8], - // value: "10.3386/w25589", - // }, - // ], - // }, - // }, - // }, - // }); } diff --git a/core/prisma/seed.ts b/core/prisma/seed.ts index a1f1c11a0..33151aad4 100644 --- a/core/prisma/seed.ts +++ b/core/prisma/seed.ts @@ -65,7 +65,7 @@ async function main() { ]; logger.info("build crocroc"); - await buildCrocCroc(db, crocCrocId); + await buildCrocCroc(crocCrocId); logger.info("build unjournal"); await buildUnjournal(prisma, unJournalId); diff --git a/core/vitest.config.ts b/core/vitest.config.ts index f2561f83b..26414b1e9 100644 --- a/core/vitest.config.ts +++ b/core/vitest.config.ts @@ -6,5 +6,13 @@ export default defineConfig({ plugins: [react(), tsconfigPaths()], test: { environment: "jsdom", + exclude: [ + "**/playwright/**", + "**/node_modules/**", + "**/dist/**", + "**/cypress/**", + "**/.{idea,git,cache,output,temp}/**", + "**/{karma,rollup,webpack,vite,vitest,jest,ava,babel,nyc,cypress,tsup,build}.config.*", + ], }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d01edf743..e6166529c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -212,7 +212,7 @@ importers: version: 0.356.0(react@18.2.0) next: specifier: 14.2.1 - version: 14.2.1(@babel/core@7.24.3)(@opentelemetry/api@1.7.0)(react-dom@18.2.0)(react@18.2.0) + version: 14.2.1(@babel/core@7.24.3)(@opentelemetry/api@1.7.0)(@playwright/test@1.43.1)(react-dom@18.2.0)(react@18.2.0) next-connect: specifier: ^1.0.0 version: 1.0.0 @@ -268,6 +268,9 @@ importers: specifier: ^3.22.4 version: 3.22.4 devDependencies: + '@playwright/test': + specifier: ^1.43.1 + version: 1.43.1 '@preconstruct/next': specifier: ^4.0.0 version: 4.0.0 @@ -5221,6 +5224,13 @@ packages: dev: false optional: true + /@playwright/test@1.43.1: + resolution: {integrity: sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA==} + engines: {node: '>=16'} + hasBin: true + dependencies: + playwright: 1.43.1 + /@preconstruct/cli@2.8.3: resolution: {integrity: sha512-4PNEPcp8REUdqZIjtpXF1fqECuHt+pIS6k0PluSRcgX0KwPtfSw407Y2B/ItndgtRD3rKHXI6cKkwh/6Mc4TXg==} hasBin: true @@ -7299,7 +7309,7 @@ packages: '@sentry/vercel-edge': 7.109.0 '@sentry/webpack-plugin': 1.21.0 chalk: 3.0.0 - next: 14.2.1(@babel/core@7.24.3)(@opentelemetry/api@1.7.0)(react-dom@18.2.0)(react@18.2.0) + next: 14.2.1(@babel/core@7.24.3)(@opentelemetry/api@1.7.0)(@playwright/test@1.43.1)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 resolve: 1.22.8 rollup: 2.78.0 @@ -8503,7 +8513,7 @@ packages: zod: optional: true dependencies: - next: 14.2.1(@babel/core@7.24.3)(@opentelemetry/api@1.7.0)(react-dom@18.2.0)(react@18.2.0) + next: 14.2.1(@babel/core@7.24.3)(@opentelemetry/api@1.7.0)(@playwright/test@1.43.1)(react-dom@18.2.0)(react@18.2.0) zod: 3.22.4 dev: false @@ -12201,6 +12211,13 @@ packages: /fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + /fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + optional: true + /fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -14617,7 +14634,7 @@ packages: next: ^14 react: ^18 dependencies: - next: 14.2.1(@babel/core@7.24.3)(@opentelemetry/api@1.7.0)(react-dom@18.2.0)(react@18.2.0) + next: 14.2.1(@babel/core@7.24.3)(@opentelemetry/api@1.7.0)(@playwright/test@1.43.1)(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 dev: false @@ -14668,7 +14685,7 @@ packages: - babel-plugin-macros dev: false - /next@14.2.1(@babel/core@7.24.3)(@opentelemetry/api@1.7.0)(react-dom@18.2.0)(react@18.2.0): + /next@14.2.1(@babel/core@7.24.3)(@opentelemetry/api@1.7.0)(@playwright/test@1.43.1)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-SF3TJnKdH43PMkCcErLPv+x/DY1YCklslk3ZmwaVoyUfDgHKexuKlf9sEfBQ69w+ue8jQ3msLb+hSj1T19hGag==} engines: {node: '>=18.17.0'} hasBin: true @@ -14688,6 +14705,7 @@ packages: dependencies: '@next/env': 14.2.1 '@opentelemetry/api': 1.7.0 + '@playwright/test': 1.43.1 '@swc/helpers': 0.5.5 busboy: 1.6.0 caniuse-lite: 1.0.30001605 @@ -15476,6 +15494,20 @@ packages: pathe: 1.1.2 dev: true + /playwright-core@1.43.1: + resolution: {integrity: sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg==} + engines: {node: '>=16'} + hasBin: true + + /playwright@1.43.1: + resolution: {integrity: sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==} + engines: {node: '>=16'} + hasBin: true + dependencies: + playwright-core: 1.43.1 + optionalDependencies: + fsevents: 2.3.2 + /polished@4.2.2: resolution: {integrity: sha512-Sz2Lkdxz6F2Pgnpi9U5Ng/WdWAUZxmHrNPoVlm3aAemxoy2Qy7LGjQg4uf8qKelDAUW94F4np3iH2YPf2qefcQ==} engines: {node: '>=10'}