diff --git a/.dockerignore b/.dockerignore index 742d1ee7a..b1a395eda 100644 --- a/.dockerignore +++ b/.dockerignore @@ -10,3 +10,7 @@ stories .prettier* LICENSE README.md +__mocks__ +test-results +.env.local.sample +dev diff --git a/.env.local.sample b/.env.local.sample index 4b1b52d09..ea33048d0 100644 --- a/.env.local.sample +++ b/.env.local.sample @@ -1,6 +1,19 @@ BASE_CANONICAL_URL= API_HOST_CLIENT= API_HOST_SERVER= +NEXT_PUBLIC_API_HOST_CLIENT= COOKIE_SECRET= ADS_SESSION_COOKIE_NAME= SCIX_SESSION_COOKIE_NAME= +NEXT_PUBLIC_ORCID_CLIENT_ID= +NEXT_PUBLIC_ORCID_API_URL= +NEXT_PUBLIC_ORCID_REDIRECT_URI= +REDIS_HOST= +REDIS_PORT= +REDIS_PASSWORD= +VERIFIED_BOTS_ACCESS_TOKEN= +UNVERIFIABLE_BOTS_ACCESS_TOKEN= +MALICIOUS_BOTS_ACCESS_TOKEN= +NEXT_PUBLIC_GTM_ID= +NEXT_PUBLIC_RECAPTCHA_SITE_KEY= +MAILSLURP_API_TOKEN= diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..2933b4465 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,11 @@ +name: CI +on: [push, pull_request] + +jobs: + unit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build and Run Unit Tests + run: ./nectar.sh --unit + diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index cb0b62d0f..da874b396 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -69,55 +69,55 @@ jobs: name: codecov-nectar path_to_write_report: ./coverage/codecov_report.txt verbose: true - e2e-tests: - timeout-minutes: 60 - runs-on: ubuntu-latest - env: - CI: true - BASE_CANONICAL_URL: ${{ vars.BASE_CANONICAL_URL }} - API_HOST_CLIENT: ${{ vars.API_HOST_CLIENT }} - API_HOST_SERVER: ${{ vars.API_HOST_SERVER }} - COOKIE_SECRET: ${{ vars.COOKIE_SECRET }} - strategy: - matrix: - node-version: [ 18 ] - steps: - - uses: actions/checkout@v3 - - uses: pnpm/action-setup@v2 - name: Install pnpm - id: pnpm-install - with: - version: 8 - run_install: false - - uses: actions/cache@v3 - name: restore/setup pnpm cache - with: - path: ~/.pnpm-store - key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} - - - name: Install dependencies - if: steps.pnpm-cache.outputs.cache-hit != 'true' - run: pnpm install - - - name: Install Playwright Browsers - run: pnpm exec playwright install --with-deps - - - name: setup environment variables - run: | - touch .env.local - echo "CI=${{ env.CI }}" >> .env.local - echo "BASE_CANONICAL_URL=${{ env.BASE_CANONICAL_URL }}" >> .env.local - echo "API_HOST_CLIENT=${{ env.API_HOST_CLIENT }}" >> .env.local - echo "API_HOST_SERVER=${{ env.API_HOST_SERVER }}" >> .env.local - echo "COOKIE_SECRET=${{ env.COOKIE_SECRET }}" >> .env.local - - - name: Run integration tests - run: pnpm integration - - - uses: actions/upload-artifact@v3 - if: always() - with: - name: playwright-report - path: playwright-report/ - retention-days: 30 +# e2e-tests: +# timeout-minutes: 60 +# runs-on: ubuntu-latest +# env: +# CI: true +# BASE_CANONICAL_URL: ${{ vars.BASE_CANONICAL_URL }} +# API_HOST_CLIENT: ${{ vars.API_HOST_CLIENT }} +# API_HOST_SERVER: ${{ vars.API_HOST_SERVER }} +# COOKIE_SECRET: ${{ vars.COOKIE_SECRET }} +# strategy: +# matrix: +# node-version: [ 18 ] +# steps: +# - uses: actions/checkout@v3 +# - uses: pnpm/action-setup@v2 +# name: Install pnpm +# id: pnpm-install +# with: +# version: 8 +# run_install: false +# - uses: actions/cache@v3 +# name: restore/setup pnpm cache +# with: +# path: ~/.pnpm-store +# key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} +# +# - name: Install dependencies +# if: steps.pnpm-cache.outputs.cache-hit != 'true' +# run: pnpm install +# +# - name: Install Playwright Browsers +# run: pnpm exec playwright install --with-deps +# +# - name: setup environment variables +# run: | +# touch .env.local +# echo "CI=${{ env.CI }}" >> .env.local +# echo "BASE_CANONICAL_URL=${{ env.BASE_CANONICAL_URL }}" >> .env.local +# echo "API_HOST_CLIENT=${{ env.API_HOST_CLIENT }}" >> .env.local +# echo "API_HOST_SERVER=${{ env.API_HOST_SERVER }}" >> .env.local +# echo "COOKIE_SECRET=${{ env.COOKIE_SECRET }}" >> .env.local +# +# - name: Run integration tests +# run: pnpm integration +# +# - uses: actions/upload-artifact@v3 +# if: always() +# with: +# name: playwright-report +# path: playwright-report/ +# retention-days: 30 diff --git a/.gitignore b/.gitignore index 681a8a03a..b1b9d1ee4 100644 --- a/.gitignore +++ b/.gitignore @@ -120,17 +120,15 @@ dist dev/ storybook-static/ .DS_Store -.idea +.idea/ .env.development .env.production -/.vitest/ -/.vitest-preview/ -/test-results/ -/playwright-report/ -/playwright/.cache/ - -# Sentry Config File -.sentryclirc +.vitest/ +.vitest-preview/ +test-results/ +playwright-report/ +playwright/.cache/ +playwright/.auth/ # Sentry Config File .sentryclirc diff --git a/Dockerfile b/Dockerfile index d50cec8fe..3cc1622ff 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,63 +1,93 @@ -FROM node:18-alpine AS base - -# Install dependencies only when needed -FROM base AS deps -# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. -RUN apk add --no-cache libc6-compat -WORKDIR /app - -ENV NODE_ENV=production - -RUN npm install -g pnpm - -# Files required by pnpm install -COPY .npmrc.* package.json pnpm-lock.yaml .pnpmfile.cjs.* ./ - -# install deps -RUN pnpm add sharp -RUN pnpm install --frozen-lockfile --ignore-scripts --no-optional - -# Rebuild the source code only when needed -FROM base AS builder -WORKDIR /app -COPY --from=deps /app/node_modules ./node_modules -COPY . . +FROM node:20-slim AS base +ENV PNPM_HOME=/pnpm ENV NEXT_TELEMETRY_DISABLED=1 ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 -ENV NODE_ENV=production - -# Add the git commit hash to the environment variables -RUN export GIT_SHA=$(git rev-parse HEAD); echo "GIT_SHA=$GIT_SHA" >> .env.local - -# ensure pnpm is available in the builder -RUN npm install -g pnpm +ENV PATH="$PNPM_HOME:/app/.bin:/app/node_modules/.bin:$PATH" +ARG GIT_SHA +ENV GIT_SHA="$GIT_SHA" +ENV PORT=8000 +ENV SENTRYCLI_SKIP_DOWNLOAD=1 +ENV HOSTNAME="0.0.0.0" + +RUN corepack enable +RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt update && apt-get --no-install-recommends install -y libc6 +USER $USER_ID +WORKDIR /app -RUN pnpm run build +FROM base as dev +COPY --link . /app +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile --ignore-scripts --no-optional +ENTRYPOINT ["pnpm", "run", "dev"] + +FROM base as unit +COPY --link vitest.config.js /app +COPY --link vitest-setup.ts /app +COPY --link package.json /app +COPY --link tsconfig.json /app +COPY --link logger /app/logger +COPY --link src /app/src +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install vitest +ENTRYPOINT ["vitest"] + +FROM base AS build_prod +COPY --link . /app +RUN mkdir -p /app/dist/cache +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile --ignore-scripts --no-optional --prod +RUN --mount=type=cache,id=nextjs,target=/app/dist/cache pnpm run build # Production image, copy all the files and run next -FROM base AS runner -WORKDIR /app - +FROM base AS prod ENV NODE_ENV=production -ENV NEXT_TELEMETRY_DISABLED=1 RUN addgroup --system --gid 1001 nodejs RUN adduser --system --uid 1001 nextjs -COPY --from=builder /app/public ./public -COPY --from=builder /app/.env.local ./ - # Automatically leverage output traces to reduce image size # https://nextjs.org/docs/advanced-features/output-file-tracing -COPY --from=builder --chown=nextjs:nodejs /app/dist/standalone ./ -COPY --from=builder --chown=nextjs:nodejs /app/dist/static ./dist/static +COPY --link --from=build_prod --chown=nextjs:nodejs /app/dist/standalone /app +COPY --link --from=build_prod --chown=nextjs:nodejs /app/dist/static /app/dist/static +COPY --link --from=build_prod --chown=nextjs:nodejs /app/public /app/public +COPY --link --from=build_prod --chown=nextjs:nodejs --chmod=777 /app/dist/cache /app/dist/cache USER nextjs - EXPOSE 8000 +ENTRYPOINT ["node", "server.js"] -ENV PORT 8000 - -CMD ["node", "server.js"] +FROM mcr.microsoft.com/playwright:v1.42.1-jammy as e2e +ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 +ENV PNPM_HOME=/pnpm +ENV PATH="$PNPM_HOME:/app/node_modules/.bin:$PATH" +ARG USER_ID=1001 +ARG GROUP_ID=1001 +ARG GIT_SHA +ENV GIT_SHA="$GIT_SHA" +ENV PORT=8000 +ENV HOSTNAME="0.0.0.0" + +RUN usermod -u "$USER_ID" pwuser +RUN usermod -g "$GROUP_ID" pwuser + +RUN corepack enable +WORKDIR /app +RUN mkdir /app/screenshots +RUN mkdir /app/test-results +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install @playwright/test playwright @faker-js/faker + +# including src so that we can use the same tsconfig.json to reconcile imports (mostly for typings) +COPY --link src /app/src +COPY --link playwright.config.ts /app +COPY --link --chown="$USER_ID:$GROUP_ID" playwright /app/playwright +COPY --link e2e /app/e2e +COPY --link tsconfig.json /app +COPY --link --from=build_prod /app/dist/standalone /app +COPY --link --from=build_prod /app/dist/static /app/dist/static +COPY --link --from=build_prod /app/public /app/public +COPY --link --from=build_prod /app/dist/cache /app/dist/cache + +ENTRYPOINT ["playwright"] +CMD ["test"] diff --git a/Dockerfile.web b/Dockerfile.web new file mode 100644 index 000000000..665d0ccf7 --- /dev/null +++ b/Dockerfile.web @@ -0,0 +1,91 @@ + +FROM node:20-slim AS base +ENV PNPM_HOME=/pnpm +ENV NEXT_TELEMETRY_DISABLED=1 +ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 +ENV PATH="$PNPM_HOME:/app/.bin:/app/node_modules/.bin:$PATH" +ARG GIT_SHA +ENV GIT_SHA="$GIT_SHA" +ENV PORT=8000 +ENV SENTRYCLI_SKIP_DOWNLOAD=1 +ENV HOSTNAME="0.0.0.0" + +RUN corepack enable +RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ + --mount=type=cache,target=/var/lib/apt,sharing=locked \ + apt update && apt-get --no-install-recommends install -y libc6 +USER $USER_ID +WORKDIR /app + +FROM base as dev +COPY --link . /app +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile --ignore-scripts --no-optional +ENTRYPOINT ["pnpm", "run", "dev"] + +FROM base as unit +COPY --link vitest.config.js /app +COPY --link vitest-setup.ts /app +COPY --link package.json /app +COPY --link tsconfig.json /app +COPY --link logger /app/logger +COPY --link src /app/src +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install vitest +ENTRYPOINT ["vitest"] + +FROM base AS build_prod +COPY --link . /app +RUN mkdir -p /app/dist/cache +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile --ignore-scripts --no-optional --prod +RUN --mount=type=cache,id=nextjs,target=/app/dist/cache pnpm run build + +# Production image, copy all the files and run next +FROM base AS prod +ENV NODE_ENV=production + +RUN addgroup --system --gid 1001 nodejs +RUN adduser --system --uid 1001 nextjs + +# Automatically leverage output traces to reduce image size +# https://nextjs.org/docs/advanced-features/output-file-tracing +COPY --link --from=build_prod --chown=nextjs:nodejs /app/dist/standalone /app +COPY --link --from=build_prod --chown=nextjs:nodejs /app/dist/static /app/dist/static +COPY --link --from=build_prod --chown=nextjs:nodejs /app/public /app/public +COPY --link --from=build_prod --chown=nextjs:nodejs --chmod=777 /app/dist/cache /app/dist/cache + +USER nextjs +EXPOSE 8000 +ENTRYPOINT ["node", "server.js"] + +FROM mcr.microsoft.com/playwright:v1.42.1-jammy as e2e +ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1 +ENV PNPM_HOME=/pnpm +ENV PATH="$PNPM_HOME:/app/node_modules/.bin:$PATH" +ARG USER_ID=1001 +ARG GROUP_ID=1001 +ARG GIT_SHA +ENV GIT_SHA="$GIT_SHA" +ENV PORT=8000 +ENV HOSTNAME="0.0.0.0" + +RUN corepack enable +USER $USER_ID +WORKDIR /app + +RUN mkdir /app/screenshots +RUN mkdir /app/test-results +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install @playwright/test playwright @faker-js/faker + +# including src so that we can use the same tsconfig.json to reconcile imports (mostly for typings) +COPY --link src /app/src +COPY --link playwright.config.ts /app +COPY --link --chown="$USER_ID:$GROUP_ID" playwright /app/playwright +COPY --link e2e /app/e2e +COPY --link tsconfig.json /app +COPY --link --from=build_prod /app/dist/standalone /app +COPY --link --from=build_prod /app/dist/static /app/dist/static +COPY --link --from=build_prod /app/public /app/public +COPY --link --from=build_prod /app/dist/cache /app/dist/cache + +ENTRYPOINT ["playwright"] +CMD ["test"] diff --git a/e2e/auth.setup.ts b/e2e/auth.setup.ts new file mode 100644 index 000000000..18a7fab0a --- /dev/null +++ b/e2e/auth.setup.ts @@ -0,0 +1,23 @@ +import { test as setup } from '@playwright/test'; + +setup.beforeEach(async ({ page }) => { + await page.goto('/user/account/login'); +}); + +setup('authenticate user 1', async ({ page }) => { + await page.getByLabel('Email').fill('scix-testing-user1-30ae2ff4-49e1-49ef-8aae-3ef9d8399a80@mailslurp.net'); + await page.getByLabel('Password').fill('Testing123!'); + await page.getByRole('button', { name: 'Submit' }).click(); + await page.waitForURL('/?notify=account-login-success'); + + await page.context().storageState({ path: 'playwright/.auth/user1.json' }); +}); + +setup('authenticate user 2', async ({ page }) => { + await page.getByLabel('Email').fill('scix-testing-user2-4e1b4600-33aa-4346-94a7-1bd9b3d64b4c@mailslurp.net'); + await page.getByLabel('Password').fill('Testing123!'); + await page.getByRole('button', { name: 'Submit' }).click(); + await page.waitForURL('/?notify=account-login-success'); + + await page.context().storageState({ path: 'playwright/.auth/user2.json' }); +}); diff --git a/e2e/libraries/entity.spec.ts b/e2e/authenticated/libraries/entity.spec.ts similarity index 93% rename from e2e/libraries/entity.spec.ts rename to e2e/authenticated/libraries/entity.spec.ts index b7371ae49..fcad896d2 100644 --- a/e2e/libraries/entity.spec.ts +++ b/e2e/authenticated/libraries/entity.spec.ts @@ -1,11 +1,10 @@ -// back button should go to whereever it came from import { expect, test } from '@playwright/test'; test.describe.configure({ mode: 'parallel', }); -test('Back button goes to landing page', async ({ page }) => { +test.skip('Back button goes to landing page', async ({ page }) => { await page.goto('/user/libraries/001', { timeout: 60000 }); await page.getByTestId('lib-back-btn').click(); @@ -13,7 +12,7 @@ test('Back button goes to landing page', async ({ page }) => { expect(page.url()).toMatch(/^.*\/user\/libraries$/); }); -test('Library metadata are correct', async ({ page }) => { +test.skip('Library metadata are correct', async ({ page }) => { await page.goto('/user/libraries/001', { timeout: 60000 }); await expect(page.getByLabel('private')).toBeVisible(); @@ -30,7 +29,7 @@ test('Library metadata are correct', async ({ page }) => { await expect(page.getByLabel('Results').getByRole('article')).toHaveCount(10); }); -test('Click on view library as search results goes to search', async ({ page }) => { +test.skip('Click on view library as search results goes to search', async ({ page }) => { await page.goto('/user/libraries/001', { timeout: 60000 }); await page.getByText('View as Search Results').click(); @@ -38,7 +37,7 @@ test('Click on view library as search results goes to search', async ({ page }) expect(page.url()).toMatch(/^.*\/search\?q=docs\(library%2F001\).*/); }); -test('Edit document annotations with admin permission', async ({ page }) => { +test.skip('Edit document annotations with admin permission', async ({ page }) => { await page.goto('/user/libraries/001', { timeout: 60000 }); // initial state are correct @@ -99,7 +98,7 @@ test('Edit document annotations with admin permission', async ({ page }) => { await expect(annotationArea2.getByLabel('cancel').first()).toBeHidden(); }); -test('View document annotations with write permission', async ({ page }) => { +test.skip('View document annotations with write permission', async ({ page }) => { await page.goto('/user/libraries/002', { timeout: 60000 }); await page.getByLabel('show abstract').first().click(); // expand abstract / annotation @@ -159,7 +158,7 @@ test('View document annotations with write permission', async ({ page }) => { await expect(annotationArea2.getByLabel('cancel').first()).toBeHidden(); }); -test('View document annotations with read permission', async ({ page }) => { +test.skip('View document annotations with read permission', async ({ page }) => { await page.goto('/user/libraries/003', { timeout: 60000 }); // can see annotation but cannot edit @@ -181,7 +180,7 @@ test('View document annotations with read permission', async ({ page }) => { await expect(annotationArea2.getByLabel('cancel').first()).toBeHidden(); }); -test('Delete selected docs from library', async ({ page }) => { +test.skip('Delete selected docs from library', async ({ page }) => { await page.goto('/user/libraries/001', { timeout: 60000 }); await page.getByTestId('document-checkbox').nth(1).check(); @@ -191,7 +190,7 @@ test('Delete selected docs from library', async ({ page }) => { await expect(page.getByLabel('Results').getByRole('article')).toHaveCount(8); }); -test('Delete all docs from library', async ({ page }) => { +test.skip('Delete all docs from library', async ({ page }) => { await page.goto('/user/libraries/001', { timeout: 60000 }); await page.getByTestId('select-all-checkbox').check(); @@ -200,7 +199,7 @@ test('Delete all docs from library', async ({ page }) => { await expect(page.getByLabel('Results').getByRole('article')).toHaveCount(0); }); -test('Public library view should have correct metadata', async ({ page }) => { +test.skip('Public library view should have correct metadata', async ({ page }) => { await page.goto('/public-libraries/001', { timeout: 60000 }); await expect(page.getByLabel('private')).toBeHidden(); // this info is hidden from public view @@ -216,7 +215,7 @@ test('Public library view should have correct metadata', async ({ page }) => { await expect(page.getByLabel('Results').getByRole('article')).toHaveCount(10); }); -test('Public library should show abstract but not annotations', async ({ page }) => { +test.skip('Public library should show abstract but not annotations', async ({ page }) => { await page.goto('/public-libraries/001', { timeout: 60000 }); await page.getByLabel('show abstract').first().click(); // expand abstract / annotation diff --git a/e2e/libraries/landing.spec.ts b/e2e/authenticated/libraries/landing.spec.ts similarity index 64% rename from e2e/libraries/landing.spec.ts rename to e2e/authenticated/libraries/landing.spec.ts index c4d964fda..9c5fe6832 100644 --- a/e2e/libraries/landing.spec.ts +++ b/e2e/authenticated/libraries/landing.spec.ts @@ -1,10 +1,74 @@ import { expect, test } from '@playwright/test'; +import { addNewLibraryFromDashboard, deleteLibraryFromDashboard, gotoAllLibraries } from './libraries.utils'; test.describe.configure({ mode: 'parallel', }); -test('Libraries show up in the table', async ({ page }) => { +test.skip('User can add, update and delete a library from the dashboard', async ({ page }) => { + await gotoAllLibraries(page); + await expect(page.locator('#main-content').getByRole('alert')).toHaveText('No libraries found'); + await addNewLibraryFromDashboard(page, { name: 'test library', public: false, description: 'test description' }); + await expect(page.getByRole('cell', { name: 'test library test description' })).toBeVisible(); + await expect(page.getByRole('cell', { name: 'private' })).toBeVisible(); + await page.getByTestId('library-action-menu').click(); + await page.getByRole('menuitem', { name: 'Settings' }).click(); + await page.getByTestId('library-name-input').click(); + await page.getByTestId('library-name-input').fill('test library updated'); + await page.getByTestId('library-desc-input').click(); + await page.getByTestId('library-desc-input').fill('test description updated'); + await page.getByTestId('library-public-switch').locator('span').first().click(); + await page.getByRole('button', { name: 'Save' }).click(); + await gotoAllLibraries(page); + await expect.soft(page.getByRole('cell', { name: 'public' })).toBeVisible(); + await deleteLibraryFromDashboard(page, { name: 'test library updated' }); + await expect(page.locator('#main-content').getByRole('alert')).toHaveText('No libraries found'); +}); + +// test('Empty account shows no libraries', async ({ page }) => { +// await gotoAllLibraries(page); +// await expect(page.locator('#main-content').getByRole('alert')).toHaveText('No libraries found'); +// }); + +// test('Sorting and Filtering a list of libraries', async ({ page }) => { +// await addTestLibraries(page); +// +// // -------- pagination ---------- +// +// const rows = page.getByTestId('libraries-table').locator('tbody > tr'); +// await expect(rows).toHaveCount(10); +// await expect(page.getByTestId('pagination-string')).toHaveText('Showing 1 to 10 of 11 results'); +// +// // go to next page +// await page.getByLabel('go to next page').click(); +// await expect(page.getByTestId('libraries-table').locator('tbody > tr')).toHaveCount(1); +// await expect(page.getByTestId('pagination-string')).toHaveText('Showing 11 to 11 of 11 results'); +// +// // show 50 per page +// await page.getByTestId('page-size-selector').selectOption('50'); +// await expect(page.getByTestId('libraries-table').locator('tbody > tr')).toHaveCount(11); +// await expect(page.getByTestId('pagination-string')).toHaveText('Showing 1 to 11 of 11 results'); +// +// // -------- filtering ---------- +// await page.locator('[id="lib-type-select"]').click(); // select dropdown +// await page.locator('[id^="react-select-lib-type-select"]').locator('[id$="-option-1"]').click(); // select 'owner' +// await expect(page.getByTestId('libraries-table').locator('tbody > tr')).toHaveCount(8); +// await expect(page.getByTestId('pagination-string')).toHaveText('Showing 1 to 8 of 8 results'); +// +// await page.locator('[id="lib-type-select"]').click(); // select dropdown +// await page.locator('[id^="react-select-lib-type-select"]').locator('[id$="-option-2"]').click(); // select 'collaborator' +// await expect(page.getByTestId('libraries-table').locator('tbody > tr')).toHaveCount(3); +// await expect(page.getByTestId('pagination-string')).toHaveText('Showing 1 to 3 of 3 results'); +// +// await page.locator('[id="lib-type-select"]').click(); // select dropdown +// await page.locator('[id^="react-select-lib-type-select"]').locator('[id$="-option-0"]').click(); // select 'all' +// await expect(page.getByTestId('libraries-table').locator('tbody > tr')).toHaveCount(10); +// await expect(page.getByTestId('pagination-string')).toHaveText('Showing 1 to 10 of 11 results'); +// +// await cleanupTestLibraries(page); +// }); + +test.skip('Libraries show up in the table', async ({ page }) => { await page.goto('/user/libraries', { timeout: 60000 }); const rows = page.getByTestId('libraries-table').locator('tbody > tr'); await expect(rows).toHaveCount(10); @@ -21,7 +85,7 @@ test('Libraries show up in the table', async ({ page }) => { await expect(page.getByTestId('pagination-string')).toHaveText('Showing 1 to 11 of 11 results'); }); -test('Filter by library type', async ({ page }) => { +test.skip('Filter by library type', async ({ page }) => { await page.goto('/user/libraries', { timeout: 60000 }); await page.locator('[id="lib-type-select"]').click(); // select dropdown await page.locator('[id^="react-select-lib-type-select"]').locator('[id$="-option-1"]').click(); // select 'owner' @@ -39,7 +103,7 @@ test('Filter by library type', async ({ page }) => { await expect(page.getByTestId('pagination-string')).toHaveText('Showing 1 to 10 of 11 results'); }); -test('Sort libraries table', async ({ page }) => { +test.skip('Sort libraries table', async ({ page }) => { await page.goto('/user/libraries', { timeout: 60000 }); await expect(page.locator('tbody > tr').nth(0).locator('td').nth(3)).toContainText('001'); @@ -56,7 +120,7 @@ test('Sort libraries table', async ({ page }) => { await expect(page.locator('tbody > tr').nth(0).locator('td').nth(3)).toContainText('test6'); }); -test('Add new library', async ({ page }) => { +test.skip('Add new library', async ({ page }) => { await page.goto('/user/libraries', { timeout: 60000 }); await page.getByTestId('add-new-lib-btn').click(); @@ -77,7 +141,7 @@ test('Add new library', async ({ page }) => { await expect(page.getByTestId('libraries-table').locator('tbody > tr')).toHaveCount(12); }); -test('Library operations - union', async ({ page }) => { +test.skip('Library operations - union', async ({ page }) => { await page.goto('/user/libraries', { timeout: 60000 }); await page.getByTestId('lib-operation-btn').click(); @@ -101,7 +165,7 @@ test('Library operations - union', async ({ page }) => { await expect(page.getByTestId('pagination-string')).toHaveText('Showing 1 to 10 of 12 results'); }); -test('Library operations - copy', async ({ page }) => { +test.skip('Library operations - copy', async ({ page }) => { await page.goto('/user/libraries', { timeout: 60000 }); await page.getByTestId('lib-operation-btn').click(); @@ -120,7 +184,7 @@ test('Library operations - copy', async ({ page }) => { await expect(page.getByTestId('libraries-table').locator('tbody > tr').nth(1).locator('td').nth(4)).toHaveText('10'); }); -test('Library operations - empty', async ({ page }) => { +test.skip('Library operations - empty', async ({ page }) => { await page.goto('/user/libraries', { timeout: 60000 }); await page.getByTestId('lib-operation-btn').click(); @@ -137,7 +201,7 @@ test('Library operations - empty', async ({ page }) => { await expect(page.getByTestId('libraries-table').locator('tbody > tr').nth(0).locator('td').nth(4)).toHaveText('0'); }); -test('Delete libraries', async ({ page }) => { +test.skip('Delete libraries', async ({ page }) => { await page.goto('/user/libraries', { timeout: 60000 }); // no permission to delete @@ -154,7 +218,7 @@ test('Delete libraries', async ({ page }) => { await expect(page.getByTestId('pagination-string')).toHaveText('Showing 1 to 10 of 10 results'); }); -test('Action menu -> settings go to library settings page', async ({ page }) => { +test.skip('Action menu -> settings go to library settings page', async ({ page }) => { await page.goto('/user/libraries', { timeout: 60000 }); await page.locator('tbody > tr').nth(0).getByTestId('library-action-menu').click(); @@ -168,7 +232,7 @@ test('Action menu -> settings go to library settings page', async ({ page }) => expect(page.url()).toContain('/settings?from=landing'); }); -test('Click on library goes to individual library page', async ({ page }) => { +test.skip('Click on library goes to individual library page', async ({ page }) => { await page.goto('/user/libraries', { timeout: 60000 }); await page.locator('tbody > tr').nth(0).click(); diff --git a/e2e/authenticated/libraries/libraries.spec.ts b/e2e/authenticated/libraries/libraries.spec.ts new file mode 100644 index 000000000..9d6d0377a --- /dev/null +++ b/e2e/authenticated/libraries/libraries.spec.ts @@ -0,0 +1,85 @@ +import { expect, test } from '@playwright/test'; +import { addNewLibraryFromDashboard, clearLibrary, createDualUserContexts, gotoAllLibraries } from './libraries.utils'; + +test.describe.configure({ + mode: 'parallel', +}); + +test.describe('user library work flow', () => { + test.beforeEach(async ({ browser }) => { + await clearLibrary(browser); + }); + + test('user can add, update and delete a library from the dashboard', async ({ page }) => { + await gotoAllLibraries(page); + + // confirm no libraries are found + await expect(page.locator('#main-content').getByRole('alert')).toHaveText('No libraries found'); + + // add the new library + await addNewLibraryFromDashboard(page, { + name: 'test library', + public: false, + description: 'test description', + }); + + // confirm the library is added + await expect(page.getByRole('cell', { name: 'test library test description' })).toBeVisible(); + await expect(page.getByRole('cell', { name: 'private' })).toBeVisible(); + + // update library metadata + await page.getByTestId('library-action-menu').click(); + await page.getByRole('menuitem', { name: 'Settings' }).click(); + await page.getByTestId('library-name-input').click(); + await page.getByTestId('library-name-input').fill('test library updated'); + await page.getByTestId('library-desc-input').click(); + await page.getByTestId('library-desc-input').fill('test description updated'); + await page.getByTestId('library-public-switch').locator('span').first().click(); + await page.getByRole('button', { name: 'Save' }).click(); + + // confirm the library is updated + await gotoAllLibraries(page); + await expect.soft(page.getByRole('cell', { name: 'public' })).toBeVisible(); + }); + + test('user can view a public library they do not own', async ({ browser }) => { + const { user1Page, user2Page, done } = await createDualUserContexts(browser); + await gotoAllLibraries(user2Page); + + // confirm no libraries are found + await expect(user2Page.locator('#main-content').getByRole('alert')).toHaveText('No libraries found'); + + // add the new library + await addNewLibraryFromDashboard(user2Page, { + name: 'test library', + public: false, + description: 'test description', + }); + + // fetch and goto the public url (currently not accessible) + await user2Page.getByTestId('library-action-menu').click(); + await user2Page.getByRole('menuitem', { name: 'Settings' }).click(); + const publicUrl = await user2Page.getByRole('link', { name: 'View as public library' }).getAttribute('href'); + await user1Page.goto(publicUrl); + + // confirm the library is not accessible + await expect(user1Page.locator('#main-content').getByRole('alert')).toHaveText( + 'ErrorYou do not have the correct permissions or the library does not exist.', + ); + + // update the library to public + await user2Page.getByTestId('library-public-switch').locator('span').first().click(); + await user2Page.getByRole('button', { name: 'Save' }).click(); + + // fetch and goto the public url (currently accessible) + await user1Page.goto(publicUrl); + + // confirm the library is accessible + await expect(user1Page.getByTestId('lib-title')).toHaveText('test library'); + + // cleanup + await user2Page.getByRole('button', { name: 'Delete Library' }).click(); + await user2Page.getByRole('button', { name: 'Cancel' }).click(); + await done(); + }); +}); diff --git a/e2e/authenticated/libraries/libraries.utils.ts b/e2e/authenticated/libraries/libraries.utils.ts new file mode 100644 index 000000000..e3154bd0a --- /dev/null +++ b/e2e/authenticated/libraries/libraries.utils.ts @@ -0,0 +1,111 @@ +import { Browser, PlaywrightTestArgs } from '@playwright/test'; + +export const clearLibrary = async (browser: Browser) => { + const { user1Page, user2Page, done } = await createDualUserContexts(browser); + + await gotoAllLibraries(user1Page); + await clearAllLibraries(user1Page); + + await gotoAllLibraries(user2Page); + await clearAllLibraries(user2Page); + + await done(); +}; + +export const clearAllLibraries = async (page: PlaywrightTestArgs['page']) => { + for (const library of await page.locator('tbody tr').all()) { + await library.getByTestId('library-action-menu').click(); + await library.getByRole('menuitem', { name: 'Delete Library' }).click(); + await page.getByTestId('confirm-del-lib-btn').click(); + } +}; + +export const gotoAllLibraries = async (page: PlaywrightTestArgs['page']) => + await page.goto('/user/libraries', { + waitUntil: 'networkidle', + }); + +export const addNewLibraryFromDashboard = async ( + page: PlaywrightTestArgs['page'], + options: { + name: string; + public: boolean; + description: string; + }, +) => { + await page.getByTestId('add-new-lib-btn').click(); + await page.getByTestId('new-library-name').click(); + await page.getByTestId('new-library-name').fill(options.name); + await page.getByTestId('new-library-name').press('Tab'); + await page.getByTestId('new-library-desc').fill(options.description); + await page.getByTestId('new-library-desc').press('Tab'); + if (options.public) { + await page.getByTestId('add-new-lib-modal').locator('span').nth(1).click(); + } + await page.getByRole('button', { name: 'Submit' }).click(); +}; + +type LibraryActionModalOptions = + | { + createNew: true; + name: string; + public: boolean; + description: string; + } + | { + createNew: false; + name: string; + }; + +const handleLibraryActionModal = async (page: PlaywrightTestArgs['page'], options: LibraryActionModalOptions) => { + if (options.createNew) { + await page.getByRole('tab', { name: 'New Library' }).click(); + await page.getByLabel('Enter a name for the new library: *').fill(options.name); + await page.getByLabel('Description:').fill(options.description); + if (options.public) { + await page.getByLabel('Add all paper(s) to Library').locator('span').nth(1).click(); + } + } else { + await page.getByTestId('library-selector').click(); + await page.getByRole('cell', { name: options.name }).click(); + } + await page.getByRole('button', { name: 'Submit' }).click(); +}; + +export const addSelectionToLibrary = async (page: PlaywrightTestArgs['page'], options: LibraryActionModalOptions) => { + await page.getByRole('button', { name: 'Bulk Actions' }).click(); + await page.getByRole('menuitem', { name: 'Add to Library' }).click(); + await handleLibraryActionModal(page, options); +}; + +export const addPaperToLibrary = async (page: PlaywrightTestArgs['page'], options: LibraryActionModalOptions) => { + await page.getByRole('button', { name: 'Add to Library' }).click(); + await handleLibraryActionModal(page, options); +}; + +export const deleteLibraryFromDashboard = async ( + page: PlaywrightTestArgs['page'], + options: { id: string; name?: never } | { name: string; id?: never }, +) => { + if (options.id) { + await page.locator(`#${options.id}`).getByTestId('library-action-menu').click(); + } + if (options.name) { + await page.getByTestId(`library-[${options.name}]`).getByTestId('library-action-menu').click(); + } + await page.getByRole('menuitem', { name: 'Delete Library' }).click(); + await page.getByTestId('confirm-del-lib-btn').click(); +}; + +export const createDualUserContexts = async (browser: Browser) => { + const user1Context = await browser.newContext({ storageState: 'playwright/.auth/user1.json' }); + const user1Page = await user1Context.newPage(); + + const user2Context = await browser.newContext({ storageState: 'playwright/.auth/user2.json' }); + const user2Page = await user2Context.newPage(); + const done = async () => { + await user1Context.close(); + await user2Context.close(); + }; + return { user1Page, user2Page, done }; +}; diff --git a/e2e/authenticated/libraries/library.setup.ts b/e2e/authenticated/libraries/library.setup.ts new file mode 100644 index 000000000..e45b32d26 --- /dev/null +++ b/e2e/authenticated/libraries/library.setup.ts @@ -0,0 +1,8 @@ +import { expect, PlaywrightTestArgs } from '@playwright/test'; + +export const librarySetup = async (page: PlaywrightTestArgs['page']) => { + await page.goto('/user/libraries'); + await expect(page.locator('#main-content').getByRole('alert')).toHaveText('No libraries found'); +}; + +export const libraryTeardown = async (page: PlaywrightTestArgs['page']) => {}; diff --git a/e2e/libraries/settings.spec.ts b/e2e/authenticated/libraries/settings.spec.ts similarity index 92% rename from e2e/libraries/settings.spec.ts rename to e2e/authenticated/libraries/settings.spec.ts index fb374ee01..a6954ffa2 100644 --- a/e2e/libraries/settings.spec.ts +++ b/e2e/authenticated/libraries/settings.spec.ts @@ -1,11 +1,10 @@ -// back button should go to whereever it came from import { expect, test } from '@playwright/test'; test.describe.configure({ mode: 'parallel', }); -test('Library settings back button goes to the landing page', async ({ page }) => { +test.skip('Library settings back button goes to the landing page', async ({ page }) => { await page.goto('/user/libraries', { timeout: 60000 }); await page.locator('tbody > tr').nth(0).getByTestId('library-action-menu').click(); @@ -21,7 +20,7 @@ test('Library settings back button goes to the landing page', async ({ page }) = expect(page.url()).toMatch(/^.*\/user\/libraries$/); }); -test('Library settings back button goes to the library page', async ({ page }) => { +test.skip('Library settings back button goes to the library page', async ({ page }) => { // enter from library entity await page.goto('/user/libraries/001', { timeout: 60000 }); await page.getByTestId('settings-btn').click(); @@ -34,7 +33,7 @@ test('Library settings back button goes to the library page', async ({ page }) = expect(page.url()).toMatch(/^.*\/user\/libraries\/001$/); }); -test('Library settings has correct information for owner', async ({ page }) => { +test.skip('Library settings has correct information for owner', async ({ page }) => { await page.goto('/user/libraries/004/settings', { timeout: 60000 }); await expect(page.getByTestId('library-name-input')).toBeEditable({ editable: true }); @@ -61,7 +60,7 @@ test('Library settings has correct information for owner', async ({ page }) => { await expect(page.getByText('Delete Library')).toBeVisible(); }); -test('Library settings has correct information for admin', async ({ page }) => { +test.skip('Library settings has correct information for admin', async ({ page }) => { await page.goto('/user/libraries/001/settings', { timeout: 60000 }); await expect(page.getByTestId('library-name-input')).toBeEnabled(); @@ -94,7 +93,7 @@ test('Library settings has correct information for admin', async ({ page }) => { await expect(page.getByText('Delete Library')).toBeHidden(); }); -test('Library with admin permission can edit but cannot delete', async ({ page }) => { +test.skip('Library with admin permission can edit but cannot delete', async ({ page }) => { await page.goto('/user/libraries/001/settings', { timeout: 60000 }); await expect(page.getByText('Delete Library')).toBeHidden(); @@ -134,7 +133,7 @@ test('Library with admin permission can edit but cannot delete', async ({ page } await expect(row.nth(1).getByLabel('public')).toBeVisible(); }); -test('User with write permission cannot edit settings', async ({ page }) => { +test.skip('User with write permission cannot edit settings', async ({ page }) => { await page.goto('/user/libraries/002/settings', { timeout: 60000 }); await expect(page.getByTestId('library-name-input')).toBeEditable({ editable: false }); @@ -151,7 +150,7 @@ test('User with write permission cannot edit settings', async ({ page }) => { await expect(page.getByText('Delete Library')).toBeHidden(); }); -test('User with read permission cannot edit settings', async ({ page }) => { +test.skip('User with read permission cannot edit settings', async ({ page }) => { await page.goto('/user/libraries/003/settings', { timeout: 60000 }); await expect(page.getByTestId('library-name-input')).toBeEditable({ editable: false }); @@ -168,7 +167,7 @@ test('User with read permission cannot edit settings', async ({ page }) => { await expect(page.getByText('Delete Library')).toBeHidden(); }); -test('Library owner can edit library metadata', async ({ page }) => { +test.skip('Library owner can edit library metadata', async ({ page }) => { await page.goto('/user/libraries/004/settings', { timeout: 60000 }); // modify @@ -204,7 +203,7 @@ test('Library owner can edit library metadata', async ({ page }) => { await expect(row.nth(3).locator('p').nth(1)).toContainText('004 updated'); }); -test('Library owner can delete library', async ({ page }) => { +test.skip('Library owner can delete library', async ({ page }) => { await page.goto('/user/libraries/004/settings', { timeout: 60000 }); await page.getByText('Delete Library').click(); await page.getByTestId('confirm-del-lib-btn').click(); @@ -214,7 +213,7 @@ test('Library owner can delete library', async ({ page }) => { await expect(page.getByTestId('pagination-string')).toHaveText('Showing 1 to 10 of 10 results'); }); -test('Owner can edit collaborators', async ({ page }) => { +test.skip('Owner can edit collaborators', async ({ page }) => { await page.goto('/user/libraries/004/settings', { timeout: 60000 }); // add user @@ -236,7 +235,7 @@ test('Owner can edit collaborators', async ({ page }) => { await expect(page.getByTestId('collab-table').locator('tbody tr')).toHaveCount(2); }); -test('Admin can edit collaborators', async ({ page }) => { +test.skip('Admin can edit collaborators', async ({ page }) => { await page.goto('/user/libraries/001/settings', { timeout: 60000 }); // add user @@ -258,19 +257,19 @@ test('Admin can edit collaborators', async ({ page }) => { await expect(page.getByTestId('collab-table').locator('tbody tr')).toHaveCount(2); }); -test('User with write permission canot edit collaborators', async ({ page }) => { +test.skip('User with write permission canot edit collaborators', async ({ page }) => { await page.goto('/user/libraries/002/settings', { timeout: 60000 }); await expect(page.getByTestId('new-collaborator-row')).toBeHidden(); }); -test('User with read permission canot edit collaborators', async ({ page }) => { +test.skip('User with read permission canot edit collaborators', async ({ page }) => { await page.goto('/user/libraries/003/settings', { timeout: 60000 }); await expect(page.getByTestId('new-collaborator-row')).toBeHidden(); }); -test('Owner can transfer library', async ({ page }) => { +test.skip('Owner can transfer library', async ({ page }) => { await page.goto('/user/libraries/004/settings', { timeout: 60000 }); await page.getByText('Transfer Ownership').click(); diff --git a/e2e/components/pager.spec.ts b/e2e/components/pager.spec.ts deleted file mode 100644 index 56ba9101d..000000000 --- a/e2e/components/pager.spec.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { test } from '@playwright/test'; - -test.use({ - baseURL: 'http://localhost:8001/iframe.html', -}); - -test.describe.configure({ - mode: 'parallel', -}); - -test('should render the pager', async ({ page }) => { - await page.goto('?args=&id=pager--default&viewMode=story'); - await page.waitForSelector('#storybook-root [role="tablist"]'); -}); - -test('should render the pager with the correct number of pages', async ({ page }) => { - await page.goto('?args=&id=pager--default&viewMode=story'); - const pager = await page.waitForSelector('#storybook-root [role="tablist"]'); - const pages = await pager.$$('[role="tab"]'); - test.expect(pages.length).toBe(6); -}); - -test('clicking on next should advance the page', async ({ page }) => { - await page.goto('?args=&id=pager--default&viewMode=story'); - await page.waitForSelector('#storybook-root [role="tablist"]'); - const next = page.getByLabel('goto next page'); - await next.click(); - const activePage = page.getByRole('tabpanel'); - test.expect(await activePage.textContent()).toBe('Page 2'); - const activeTab = page.getByRole('tab', { selected: true }); - await test.expect(activeTab).toHaveText('●'); - await test.expect(activeTab).toHaveAttribute('data-index', '1'); -}); - -test('clicking on previous should go back a page, or cycle around', async ({ page }) => { - await page.goto('?args=&id=pager--default&viewMode=story'); - await page.waitForSelector('#storybook-root [role="tablist"]'); - const previous = page.getByLabel('goto previous page'); - await previous.click(); - const activePage = page.getByRole('tabpanel'); - test.expect(await activePage.textContent()).toBe('Page 6'); - const activeTab = page.getByRole('tab', { selected: true }); - await test.expect(activeTab).toHaveText('●'); - await test.expect(activeTab).toHaveAttribute('data-index', '5'); -}); - -test('should cycle through pages correctly in both directions', async ({ page }) => { - await page.goto('?args=&id=pager--default&viewMode=story'); - await page.waitForSelector('#storybook-root [role="tablist"]'); - - const check = async (i: number) => { - const activePage = page.getByRole('tabpanel'); - test.expect(await activePage.textContent()).toBe(`Page ${i + 1}`); - const activeTab = page.getByRole('tab', { selected: true }); - await test.expect(activeTab).toHaveText('●'); - await test.expect(activeTab).toHaveAttribute('data-index', `${i}`); - }; - - for (let i = 0; i < 5; i++) { - await check(i); - const next = page.getByLabel('goto next page'); - await next.click(); - } - for (let i = 5; i >= 0; i--) { - await check(i); - const previous = page.getByLabel('goto previous page'); - await previous.click(); - } -}); - -test('dynamic page content works properly', async ({ page }) => { - await page.goto('?args=&id=pager--with-dynamic-content&viewMode=story'); - await page.waitForSelector('#storybook-root [role="tablist"]'); - const text = await page.getByRole('tabpanel').textContent(); - test.expect(JSON.parse(text)).toStrictEqual({ title: 'First', page: 0 }); - await page.getByLabel('goto next page').click(); - const text2 = await page.getByRole('tabpanel').textContent(); - test.expect(JSON.parse(text2)).toStrictEqual({ title: 'Second', page: 1 }); -}); diff --git a/e2e/workflows/search.spec.ts b/e2e/workflows/search.spec.ts new file mode 100644 index 000000000..a2ca75579 --- /dev/null +++ b/e2e/workflows/search.spec.ts @@ -0,0 +1,18 @@ +import { expect, test } from '@playwright/test'; + +test.describe.configure({ + mode: 'parallel', +}); + +test('Basic Search Workflows', async ({ page }) => { + await page.goto('/'); + await page.getByTestId('search-input').fill('identifier:1985ARA&A..23..169D'); + await page.getByTestId('search-input').press('Enter'); + await page.waitForURL('/search?q=identifier%3A1985ARA%26A..23..169D&sort=score+desc&sort=date+desc&p=1', { + waitUntil: 'load', + }); + await page.getByRole('link', { name: 'Radio emission from the sun and stars.' }).click(); + await page.waitForURL('/abs/1985ARA&A..23..169D/abstract', { waitUntil: 'load' }); + await page.getByRole('button', { name: 'Full Text Sources' }).click(); + await page.getByRole('menuitem', { name: 'ADS PDF' }).click(); +}); diff --git a/global.d.ts b/global.d.ts index 8c98f5580..fcd369de9 100644 --- a/global.d.ts +++ b/global.d.ts @@ -48,6 +48,9 @@ declare global { GIT_SHA: string; CSP_REPORT_URI: string; CSP_REPORT_ONLY: string; + MAILSLURP_API_KEY: string; + MAILSLURP_INBOX_ID: string; + MAILSLURP_DOMAIN: string; } } } diff --git a/logger/logger.ts b/logger/logger.ts index 435c4ff2e..87aa9920f 100644 --- a/logger/logger.ts +++ b/logger/logger.ts @@ -5,7 +5,7 @@ export const logger: Logger = pino({ browser: { asObject: true, }, - level: process.env.NODE_ENV === 'development' ? 'debug' : 'info', + level: process.env.NODE_ENV === 'development' ? 'trace' : 'info', base: { env: process.env.NODE_ENV || 'development', }, diff --git a/nectar.sh b/nectar.sh new file mode 100755 index 000000000..96057a858 --- /dev/null +++ b/nectar.sh @@ -0,0 +1,302 @@ +#!/usr/bin/env bash + +set -o nounset +set -o errexit +set -o errtrace +set -o pipefail +IFS=$'\n\t' +_ME="$(basename "${0}")" +__DEBUG_COUNTER=0 +_GIT_SHA=$(git rev-parse HEAD) + +_debug() { + if ((${_USE_DEBUG:-0})) + then + __DEBUG_COUNTER=$((__DEBUG_COUNTER+1)) + { + # Prefix debug message with "bug (U+1F41B)" + printf "🐛 %s " "${__DEBUG_COUNTER}" + "${@}" + printf "―――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――\\n" + } 1>&2 + fi +} + +_exit_1() { + { + printf "%s " "$(tput setaf 1)!$(tput sgr0)" + "${@}" + } 1>&2 + exit 1 +} + +_warn() { + { + printf "%s " "$(tput setaf 1)!$(tput sgr0)" + "${@}" + } 1>&2 +} + +_print_help() { + cat <] + ${_ME} -h | --help + +Options: + -h, --help Display this help information. + -p, --prod Build and start a production server container + -i, --e2e Build and run the integration tests in container + --ui Run the playwright UI for integration tests + -u, --unit Build and run the unit tests + -d, --dev Build and run a dev server + -s, --skip Skip building the image and only run the container + --ci Run the containers in CI mode + --silent Silence script debug logs +HEREDOC +} + + +# Initialize program option variables. +_PRINT_HELP=0 +_USE_DEBUG=1 + +# Initialize additional expected option variables. +_E2E= +_PROD= +_SKIP_BUILD= +_UNIT= +_DEV= +_UI= +_CI= + +while ((${#})) +do + __arg="${1:-}" + + case "${__arg}" in + -h|--help) + _PRINT_HELP=1 + ;; + --silent) + _USE_DEBUG=0 + ;; + -i|--e2e) + _E2E=1 + ;; + --ui) + _E2E=1 + _UI=1 + ;; + -p|--prod) + _PROD=1 + ;; + -u|--unit) + _UNIT=1 + ;; + -d|--dev) + _DEV=1 + ;; + -s|--skip) + _SKIP_BUILD=1 + ;; + --ci) + _CI=1 + ;; + -is) + _E2E=1 + _SKIP_BUILD=1 + ;; + -ds) + _DEV=1 + _SKIP_BUILD=1 + ;; + -us) + _UNIT=1 + _SKIP_BUILD=1 + ;; + --) + # Terminate option parsing. + shift + break + ;; + -*) + _exit_1 printf "Unexpected option: %s\\n" "${__arg}" + ;; + esac + + shift +done + +############################################################################### +# Program Functions +############################################################################### + +_prod() { + _ADDIT_ARGS="$*" + _TARGET=prod + + if [[ -z "${_SKIP_BUILD}" ]] + then + _debug printf ">> production server container building...\\n" + docker build -t "nectar-${_TARGET}:${_GIT_SHA}" \ + --target="${_TARGET}" \ + --build-arg USER_ID="$(id -u)" \ + --build-arg GROUP_ID="$(id -g)" \ + --build-arg GIT_SHA="${_GIT_SHA}" \ + "$(pwd)" + else + _debug printf ">> skipping build\\n" + fi + + _debug printf ">> production server starting...\\n" + docker run -it --rm --name nectar \ + --env-file .env.local \ + -p 8000:8000 \ + --name "nectar-${_TARGET}" \ + "nectar-${_TARGET}:${_GIT_SHA}" \ + "${_ADDIT_ARGS}" +} + +_e2e() { + _ADDIT_ARGS="$*" + _TARGET=e2e + + if [[ -z "${_SKIP_BUILD}" ]]; then + _debug printf ">> integration tests container building...\\n" + docker build -t "nectar-${_TARGET}:${_GIT_SHA}" \ + --target="${_TARGET}" \ + --build-arg USER_ID="$(id -u)" \ + --build-arg GROUP_ID="$(id -g)" \ + --build-arg GIT_SHA="${_GIT_SHA}" \ + "$(pwd)" + else + _debug printf ">> skipping build\\n" + fi + + if [[ -n "${_UI}" ]]; then + _debug printf ">> integration tests (UI) server is starting...\\n" + docker run -it --rm \ + -v "$(pwd)"/playwright:/app/playwright \ + -v "$(pwd)"/screenshots:/app/screenshots \ + -v "$(pwd)"/test-results:/app/test-results \ + -v "$(pwd)"/playwright.config.ts:/app/playwright.config.ts \ + -v "$(pwd)"/e2e:/app/e2e \ + -p 3000:3000 \ + --env-file .env.local \ + --name "nectar-${_TARGET}" \ + "nectar-${_TARGET}:${_GIT_SHA}" \ + test --ui --ui-host=0.0.0.0 --ui-port=3000 + else + _debug printf ">> integration tests running...\\n" + docker run -it --rm \ + -v "$(pwd)"/playwright:/app/playwright \ + -v "$(pwd)"/screenshots:/app/screenshots \ + -v "$(pwd)"/test-results:/app/test-results \ + -v "$(pwd)"/playwright.config.ts:/app/playwright.config.ts \ + -v "$(pwd)"/e2e:/app/e2e \ + --env-file .env.local \ + --name "nectar-${_TARGET}" \ + "nectar-${_TARGET}:${_GIT_SHA}" + fi +} + +_unit() { + _TARGET=unit + _ADDIT_ARGS="$*" + + if [[ -z "${_SKIP_BUILD}" ]] + then + _debug printf ">> unit tests container building...\\n" + docker build -t "nectar-${_TARGET}:${_GIT_SHA}" \ + --target="${_TARGET}" \ + "$(pwd)" + else + _debug printf ">> skipping build\\n" + fi + + _debug printf ">> unit tests running...\\n" + docker run -it --rm \ + -v "$(pwd)"/.vitest:/app/.vitest \ + -v "$(pwd)"/src:/app/src \ + --name "nectar-${_TARGET}" \ + "nectar-${_TARGET}:${_GIT_SHA}" \ + "${_ADDIT_ARGS}" +} + +_dev() { + _TARGET=dev + _ADDIT_ARGS="$*" + + if [[ -z "${_SKIP_BUILD}" ]] + then + _debug printf ">> dev container building...\\n" + docker build -t "nectar-${_TARGET}" \ + --target="${_TARGET}" \ + "$(pwd)" + else + _debug printf ">> skipping build\\n" + fi + + _debug printf ">> dev server running...\\n" + docker run -it --rm \ + -v "$(pwd)":/app \ + -p 8000:8000 \ + --env-file .env.local \ + --name "nectar-${_TARGET}" \ + "nectar-${_TARGET}" + "nectar-${_TARGET}:${GIT_SHA}" +} + +_simple() { + export DOCKER_BUILDKIT=1 + + if [[ -n "${_PROD}" ]]; then + _prod "$@" + fi + + if [[ -n "${_E2E}" ]]; then + _e2e "$@" + fi + + if [[ -n "${_UNIT}" ]]; then + _unit "$@" + fi + + if [[ -n "${_DEV}" ]]; then + _dev "$@" + fi +} + +############################################################################### +# Main +############################################################################### + +# _main() +# +# Usage: +# _main [] [] +# +# Description: +# Entry point for the program, handling basic option parsing and dispatching. +_main() { + if ((_PRINT_HELP)) + then + _print_help + else + _simple "$@" + fi +} + +# Call `_main` after everything has been defined. +_main "$@" diff --git a/next.config.js b/next.config.js index 88d93f587..48c852d93 100644 --- a/next.config.js +++ b/next.config.js @@ -6,7 +6,7 @@ const { withSentryConfig } = require('@sentry/nextjs'); const config = { distDir: process.env.DIST_DIR || 'dist', generateBuildId: async () => { - return process.env.GIT_SHA ?? ''; + return process.env.GIT_SHA || 'latest'; }, generateEtags: true, poweredByHeader: false, @@ -161,6 +161,7 @@ const sentryConfig = [ // https://docs.sentry.io/product/crons/ // https://vercel.com/docs/cron-jobs automaticVercelMonitors: false, + release: process.env.GIT_SHA || 'latest', }, ]; diff --git a/playwright.config.ts b/playwright.config.ts index 27526aecf..fa37b21bf 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,7 +1,4 @@ import { defineConfig, devices } from '@playwright/test'; -import dotenv from 'dotenv'; - -dotenv.config(); /** * See https://playwright.dev/docs/test-configuration. @@ -20,57 +17,78 @@ export default defineConfig({ reporter: process.env.CI ? 'github' : 'list', /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ use: { + actionTimeout: 0, + /* Base URL to use in actions like `await page.goto('/')`. */ - baseURL: 'http://localhost:8000', + baseURL: `http://localhost:${process.env.PORT || 8000}`, + bypassCSP: true, /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', + trace: 'retain-on-failure', + video: process.env.CI ? 'off' : 'on', + headless: true, }, /* Configure projects for major browsers */ projects: [ + { name: 'auth-setup', testMatch: /auth.setup\.ts/ }, { - name: 'chromium', + name: 'logged-out-chrome', + testMatch: '**/*.spec.ts', + testIgnore: '**/authenticated/**/*.spec.ts', use: { ...devices['Desktop Chrome'], }, }, - { - name: 'firefox', - use: { ...devices['Desktop Firefox'] }, - }, - - { - name: 'webkit', - use: { ...devices['Desktop Safari'] }, + name: 'logged-in-chrome', + testMatch: '**/authenticated/**/*.spec.ts', + use: { + ...devices['Desktop Chrome'], + storageState: 'playwright/.auth/user1.json', + }, + dependencies: ['auth-setup'], }, ], webServer: [ { - command: 'pnpm storybook dev --ci --quiet --disable-telemetry --port 8001', - timeout: 300000, - stdout: 'ignore', - stderr: 'pipe', - reuseExistingServer: !process.env.CI, - url: 'http://localhost:8001', - }, - { - env: { - BASE_CANONICAL_URL: process.env.BASE_CANONICAL_URL || 'https://ui.adsabs.harvard.edu', - API_HOST_CLIENT: process.env.API_HOST_CLIENT || 'https://devapi.adsabs.harvard.edu/v1', - API_HOST_SERVER: process.env.API_HOST_SERVER || 'https://devapi.adsabs.harvard.edu/v1', - COOKIE_SECRET: process.env.COOKIE_SECRET || 'secret_secret_secret_secret_secret', - ADS_SESSION_COOKIE_NAME: process.env.ADS_SESSION_COOKIE_NAME || 'ads_session', - SCIX_SESSION_COOKIE_NAME: process.env.SCIX_SESSION_COOKIE_NAME || 'scix_session', - }, - command: 'pnpm run dev:mocks', - // 5 minute timeout + command: 'node server.js', timeout: 300000, - reuseExistingServer: !process.env.CI, stdout: 'ignore', - stderr: 'pipe', + stderr: 'ignore', url: 'http://localhost:8000', }, + // { + // command: 'pnpm storybook dev --ci --quiet --disable-telemetry --port 8001', + // timeout: 300000, + // stdout: 'ignore', + // stderr: 'pipe', + // reuseExistingServer: !process.env.CI, + // url: 'http://localhost:8001', + // }, + // { + // env: { + // BASE_CANONICAL_URL: process.env.BASE_CANONICAL_URL || 'https://ui.adsabs.harvard.edu', + // API_HOST_CLIENT: process.env.API_HOST_CLIENT || 'https://devapi.adsabs.harvard.edu/v1', + // API_HOST_SERVER: process.env.API_HOST_SERVER || 'https://devapi.adsabs.harvard.edu/v1', + // COOKIE_SECRET: process.env.COOKIE_SECRET || 'secret_secret_secret_secret_secret', + // ADS_SESSION_COOKIE_NAME: process.env.ADS_SESSION_COOKIE_NAME || 'ads_session', + // SCIX_SESSION_COOKIE_NAME: process.env.SCIX_SESSION_COOKIE_NAME || 'scix_session', + // }, + // command: 'node dist/server.js', + // // 5 minute timeout + // timeout: 300000, + // reuseExistingServer: !process.env.CI, + // stdout: 'ignore', + // stderr: 'pipe', + // url: 'http://localhost:8000', + // }, + // { + // command: `PORT=${process.env.PORT || 8000} node dist/standalone/server.js`, + // url: `http://localhost:${process.env.PORT || 8000}`, + // timeout: 5 * 60 * 1000, + // stdout: 'pipe', + // stderr: 'pipe', + // }, ], }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c1a53f045..887871674 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.0' +lockfileVersion: '6.1' settings: autoInstallPeers: true @@ -6782,6 +6782,8 @@ packages: peerDependenciesMeta: '@next/font': optional: true + '@storybook/addon-actions': + optional: true typescript: optional: true webpack: diff --git a/screenshots/doesitwork.png b/screenshots/doesitwork.png new file mode 100644 index 000000000..ad19bb61b Binary files /dev/null and b/screenshots/doesitwork.png differ diff --git a/screenshots/google.png b/screenshots/google.png new file mode 100644 index 000000000..4995a7dcf Binary files /dev/null and b/screenshots/google.png differ diff --git a/screenshots/screenshot.png b/screenshots/screenshot.png new file mode 100644 index 000000000..4a984a840 Binary files /dev/null and b/screenshots/screenshot.png differ diff --git a/screenshots/test.png b/screenshots/test.png new file mode 100644 index 000000000..4a984a840 Binary files /dev/null and b/screenshots/test.png differ diff --git a/src/components/Libraries/LibraryListTable.tsx b/src/components/Libraries/LibraryListTable.tsx index a70c592af..162dad4e0 100644 --- a/src/components/Libraries/LibraryListTable.tsx +++ b/src/components/Libraries/LibraryListTable.tsx @@ -9,26 +9,26 @@ import { UpDownIcon, } from '@chakra-ui/icons'; import { + Box, + Button, + Center, + Flex, + IconButton, + Menu, + MenuButton, + MenuItem, + MenuList, Table, TableProps, Tbody, Td, + Text, Th, Thead, - Tr, - Flex, - Text, Tooltip, - Center, - Menu, - MenuButton, - MenuItem, - MenuList, - useToast, - Button, + Tr, useBreakpoint, - Box, - IconButton, + useToast, } from '@chakra-ui/react'; import { ControlledPaginationControls } from '@components'; import { CustomInfoMessage } from '@components/Feedbacks'; @@ -245,6 +245,8 @@ export const LibraryListTable = (props: ILibraryListTableProps) => { _hover={{ backgroundColor: colors.highlightBackground, color: colors.highlightForeground }} onClick={() => onLibrarySelect(id)} tabIndex={0} + data-testid={`library-[${name}]`} + id={id} onKeyDown={(e) => { if (e.key === 'Enter') { onLibrarySelect(id); diff --git a/src/pages/user/account/login.tsx b/src/pages/user/account/login.tsx index 0bd9ae92f..4f3a86138 100644 --- a/src/pages/user/account/login.tsx +++ b/src/pages/user/account/login.tsx @@ -81,7 +81,7 @@ const Login: NextPage = () => {
- Email + Email { /> - Password + Password