diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2aa5cae0ab..4d44a020d8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -267,6 +267,15 @@ jobs: yarn rw test api --no-watch working-directory: ${{ steps.set-up-test-project.outputs.test-project-path }} + - name: Run db import tracking tests + shell: bash + run: | + # Only run this for ESM projects where the vitest config file exists + if test -f ./vitest-sort.config.ts; then + npx vitest --config ./vitest-sort.config.ts run + fi + working-directory: ${{ steps.set-up-test-project.outputs.test-project-path }}/api + - name: 🖥️ Run serve smoke tests working-directory: tasks/smoke-tests/serve run: npx playwright test diff --git a/__fixtures__/esm-fragment-test-project/.editorconfig b/__fixtures__/esm-fragment-test-project/.editorconfig new file mode 100644 index 0000000000..ae10a5cce3 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/.editorconfig @@ -0,0 +1,10 @@ +# editorconfig.org +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_size = 2 +indent_style = space +insert_final_newline = true +trim_trailing_whitespace = true diff --git a/__fixtures__/esm-fragment-test-project/.env.defaults b/__fixtures__/esm-fragment-test-project/.env.defaults new file mode 100644 index 0000000000..fb88fb33b3 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/.env.defaults @@ -0,0 +1,19 @@ +# These environment variables will be used by default if you do not create any +# yourself in .env. This file should be safe to check into your version control +# system. Any custom values should go in .env and .env should *not* be checked +# into version control. + +# schema.prisma defaults +DATABASE_URL=file:./dev.db + +# location of the test database for api service scenarios (defaults to ./.redwood/test.db if not set) +# TEST_DATABASE_URL=file:./.redwood/test.db + +# disables Prisma CLI update notifier +PRISMA_HIDE_UPDATE_MESSAGE=true + +# Option to override the current environment's default api-side log level +# See: https://redwoodjs.com/docs/logger for level options, defaults to "trace" otherwise. +# Most applications want "debug" or "info" during dev, "trace" when you have issues and "warn" in production. +# Ordered by how verbose they are: trace | debug | info | warn | error | silent +# LOG_LEVEL=debug diff --git a/__fixtures__/esm-fragment-test-project/.env.example b/__fixtures__/esm-fragment-test-project/.env.example new file mode 100644 index 0000000000..2a2de6c026 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/.env.example @@ -0,0 +1,4 @@ +# DATABASE_URL=file:./dev.db +# TEST_DATABASE_URL=file:./.redwood/test.db +# PRISMA_HIDE_UPDATE_MESSAGE=true +# LOG_LEVEL=trace diff --git a/__fixtures__/esm-fragment-test-project/.gitignore b/__fixtures__/esm-fragment-test-project/.gitignore new file mode 100644 index 0000000000..31d9637ede --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/.gitignore @@ -0,0 +1,24 @@ +.idea +.DS_Store +.env* +!.env.example +!.env.defaults +.netlify +.redwood/* +!.redwood/README.md +dev.db* +dist +dist-babel +node_modules +yarn-error.log +web/public/mockServiceWorker.js +web/types/graphql.d.ts +api/types/graphql.d.ts +api/src/lib/generateGraphiQLHeader.* +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/sdks +!.yarn/versions diff --git a/__fixtures__/esm-fragment-test-project/.redwood/README.md b/__fixtures__/esm-fragment-test-project/.redwood/README.md new file mode 100644 index 0000000000..8a1bf5738b --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/.redwood/README.md @@ -0,0 +1,44 @@ +# .redwood + +## What is this directory? + +Redwood uses this `.redwood` directory to store transitory data that aids in the smooth and convenient operation of your Redwood project. + +## Do I need to do anything with this directory? + +No. You shouldn't have to create, edit or delete anything in this directory in your day-to-day work with Redwood. + +You don't need to commit any other contents of this directory to your version control system. It's ignored by default. + +## What's in this directory? + +### Files + +| Name | Description | +| :---------------- | :----------------------------------------------------------------------------------------------------------------- | +| commandCache.json | This file contains mappings to assist the Redwood CLI in efficiently executing commands. | +| schema.graphql | This is the GraphQL schema which has been automatically generated from your Redwood project. | +| telemetry.txt | Contains a unique ID used for telemetry. This value is rotated every 24 hours to protect your project's anonymity. | +| test.db | The sqlite database used when running tests. | + +### Directories + +| Name | Description | +| :---------- | :----------------------------------------------------------------------------------------------------------------------------------------------- | +| locks | Stores temporary files that Redwood uses to keep track of the execution of async/background tasks between processes. | +| logs | Stores log files for background tasks such as update checking. | +| prebuild | Stores transpiled JavaScript that is generated as part of Redwood's build process. | +| telemetry | Stores the recent telemetry that the Redwood CLI has generated. You may inspect these files to see everything Redwood is anonymously collecting. | +| types | Stores the results of type generation. | +| updateCheck | Stores a file which contains the results of checking for Redwood updates. | +| studio | Used to store data for `rw studio` | + +We try to keep this README up to date but you may, from time to time, find other files or directories in this `.redwood` directory that have not yet been documented here. This is likely nothing to worry about but feel free to let us know and we'll update this list. + +### Telemetry + +RedwoodJS collects completely anonymous telemetry data about general usage. For transparency, that data is viewable in the respective directories and files. To learn more and manage your project's settings, visit [telemetry.redwoodjs.com](https://telemetry.redwoodjs.com). + +### Have any questions? + +Feel free to reach out to us in the [RedwoodJS Community](https://community.redwoodjs.com/) forum if you have any questions. diff --git a/__fixtures__/esm-fragment-test-project/.vscode/extensions.json b/__fixtures__/esm-fragment-test-project/.vscode/extensions.json new file mode 100644 index 0000000000..6e458a9231 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/.vscode/extensions.json @@ -0,0 +1,16 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint", + "eamodio.gitlens", + "ofhumanbondage.react-proptypes-intellisense", + "mgmcdermott.vscode-language-babel", + "wix.vscode-import-cost", + "pflannery.vscode-versionlens", + "editorconfig.editorconfig", + "prisma.prisma", + "graphql.vscode-graphql", + "csstools.postcss", + "bradlc.vscode-tailwindcss" + ], + "unwantedRecommendations": [] +} \ No newline at end of file diff --git a/__fixtures__/esm-fragment-test-project/.vscode/launch.json b/__fixtures__/esm-fragment-test-project/.vscode/launch.json new file mode 100644 index 0000000000..37257c5c15 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/.vscode/launch.json @@ -0,0 +1,56 @@ +{ + "version": "0.3.0", + "configurations": [ + { + "command": "yarn redwood dev --apiDebugPort 18911", // you can add --fwd='--open=false' to prevent the browser from opening + "name": "Run Dev Server", + "request": "launch", + "type": "node-terminal" + }, + { + "name": "Attach API debugger", + "port": 18911, // you can change this port, see https://redwoodjs.com/docs/project-configuration-dev-test-build#debugger-configuration + "request": "attach", + "skipFiles": [ + "/**" + ], + "type": "node", + "localRoot": "${workspaceFolder}/node_modules/@cedarjs/api-server/dist", + "remoteRoot": "${workspaceFolder}/node_modules/@cedarjs/api-server/dist", + "sourceMaps": true, + "restart": true, + "preLaunchTask": "WaitForDevServer", + }, + { + "name": "Launch Web debugger", + "type": "chrome", + "request": "launch", + "url": "http://localhost:8910", + "webRoot": "${workspaceRoot}/web/src", + "preLaunchTask": "WaitForDevServer", + }, + { + "command": "yarn redwood test api", + "name": "Test api", + "request": "launch", + "type": "node-terminal" + }, + { + "command": "yarn redwood test web", + "name": "Test web", + "request": "launch", + "type": "node-terminal" + }, + ], + "compounds": [ + { + "name": "Start Debug", + "configurations": [ + "Run Dev Server", + "Attach API debugger", + "Launch Web debugger" + ], + "stopAll": true + } + ] +} diff --git a/__fixtures__/esm-fragment-test-project/.vscode/settings.json b/__fixtures__/esm-fragment-test-project/.vscode/settings.json new file mode 100644 index 0000000000..1d3afa8ae3 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/.vscode/settings.json @@ -0,0 +1,17 @@ +{ + "editor.tabSize": 2, + "files.trimTrailingWhitespace": true, + "editor.formatOnSave": false, + "editor.codeActionsOnSave": { + "source.fixAll.eslint": "explicit" + }, + "[prisma]": { + "editor.formatOnSave": true + }, + "tailwindCSS.classAttributes": [ + "class", + "className", + "activeClassName", + "errorClassName" + ] +} diff --git a/__fixtures__/esm-fragment-test-project/.vscode/tasks.json b/__fixtures__/esm-fragment-test-project/.vscode/tasks.json new file mode 100644 index 0000000000..549249ec63 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/.vscode/tasks.json @@ -0,0 +1,29 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "WaitForDevServer", + "group": "none", + "type": "shell", + "command": "bash", + "args": [ + "-c", + "while ! echo -n > /dev/tcp/localhost/18911; do sleep 1; done;" + ], + "windows": { + "command": "powershell", + "args": [ + "-NoProfile", + "-ExecutionPolicy", "Bypass", + "while (-not (Test-NetConnection -ComputerName localhost -Port 18911)) { Start-Sleep -Seconds 1 };" + ] + }, + "presentation": { + "reveal": "silent", + "revealProblems": "onProblem", + "panel": "shared", + "close": true + } + }, + ] +} diff --git a/__fixtures__/esm-fragment-test-project/.yarnrc.yml b/__fixtures__/esm-fragment-test-project/.yarnrc.yml new file mode 100644 index 0000000000..e8c5d50aa7 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/.yarnrc.yml @@ -0,0 +1,15 @@ +# Yarn's manifest file. You can configure yarn here. +# See https://yarnpkg.com/configuration/yarnrc. + +# For `node_modules` (see `nodeLinker` below), this is almost always the preferred option. +compressionLevel: 0 + +enableGlobalCache: true + +# Lets yarn use hardlinks inside `node_modules` to dedupe packages. +# For a more pnpm-like experience, consider `hardlinks-global` where hardlinks point to a global store. +nmMode: hardlinks-local + +# How to install Node packages. +# Heads up: right now, Redwood expects this to be `node-modules`. +nodeLinker: node-modules diff --git a/__fixtures__/esm-fragment-test-project/README.md b/__fixtures__/esm-fragment-test-project/README.md new file mode 100644 index 0000000000..4487e7f63d --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/README.md @@ -0,0 +1,17 @@ +# README + +Welcome to your new [CedarJS](https://cedarjs.com) project! + +Start by installing dependencies: + +``` +yarn install +``` + +Then start the development server: + +``` +yarn redwood dev +``` + +Your browser should automatically open to [http://localhost:8910](http://localhost:8910) where you'll see the Welcome Page, which links out to many great resources. diff --git a/__fixtures__/esm-fragment-test-project/api/db/migrations/20220101120000_create_post_user/migration.sql b/__fixtures__/esm-fragment-test-project/api/db/migrations/20220101120000_create_post_user/migration.sql new file mode 100644 index 0000000000..9dd73df9b6 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/db/migrations/20220101120000_create_post_user/migration.sql @@ -0,0 +1,34 @@ +-- CreateTable +CREATE TABLE "UserExample" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "email" TEXT NOT NULL, + "name" TEXT +); + +-- CreateTable +CREATE TABLE "Post" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "title" TEXT NOT NULL, + "body" TEXT NOT NULL, + "authorId" INTEGER NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "User" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "email" TEXT NOT NULL, + "hashedPassword" TEXT NOT NULL, + "fullName" TEXT NOT NULL, + "salt" TEXT NOT NULL, + "resetToken" TEXT, + "resetTokenExpiresAt" DATETIME, + "roles" TEXT +); + +-- CreateIndex +CREATE UNIQUE INDEX "UserExample_email_key" ON "UserExample"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); diff --git a/__fixtures__/esm-fragment-test-project/api/db/migrations/20220102120000_create_contact/migration.sql b/__fixtures__/esm-fragment-test-project/api/db/migrations/20220102120000_create_contact/migration.sql new file mode 100644 index 0000000000..8d7bd91beb --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/db/migrations/20220102120000_create_contact/migration.sql @@ -0,0 +1,8 @@ +-- CreateTable +CREATE TABLE "Contact" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "name" TEXT NOT NULL, + "email" TEXT NOT NULL, + "message" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); diff --git a/__fixtures__/esm-fragment-test-project/api/db/migrations/20250731152424_create_produce_stall/migration.sql b/__fixtures__/esm-fragment-test-project/api/db/migrations/20250731152424_create_produce_stall/migration.sql new file mode 100644 index 0000000000..f7f6352f3e --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/db/migrations/20250731152424_create_produce_stall/migration.sql @@ -0,0 +1,28 @@ +-- CreateTable +CREATE TABLE "Produce" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "quantity" INTEGER NOT NULL, + "price" INTEGER NOT NULL, + "nutrients" TEXT, + "region" TEXT NOT NULL, + "isSeedless" BOOLEAN, + "ripenessIndicators" TEXT, + "vegetableFamily" TEXT, + "isPickled" BOOLEAN, + "stallId" TEXT NOT NULL, + CONSTRAINT "Produce_stallId_fkey" FOREIGN KEY ("stallId") REFERENCES "Stall" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Stall" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "stallNumber" TEXT NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "Produce_name_key" ON "Produce"("name"); + +-- CreateIndex +CREATE UNIQUE INDEX "Stall_stallNumber_key" ON "Stall"("stallNumber"); diff --git a/__fixtures__/esm-fragment-test-project/api/db/migrations/migration_lock.toml b/__fixtures__/esm-fragment-test-project/api/db/migrations/migration_lock.toml new file mode 100644 index 0000000000..e5e5c4705a --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/db/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (i.e. Git) +provider = "sqlite" \ No newline at end of file diff --git a/__fixtures__/esm-fragment-test-project/api/db/schema.prisma b/__fixtures__/esm-fragment-test-project/api/db/schema.prisma new file mode 100644 index 0000000000..bf7e70169a --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/db/schema.prisma @@ -0,0 +1,79 @@ +// Don't forget to tell Prisma about your edits to this file using +// `yarn rw prisma migrate dev` or `yarn rw prisma db push`. +// `migrate` is like committing while `push` is for prototyping. +// Read more about both here: +// https://www.prisma.io/docs/orm/prisma-migrate + +datasource db { + provider = "sqlite" + url = env("DATABASE_URL") +} + +generator client { + provider = "prisma-client-js" + binaryTargets = "native" +} + +// Define your own datamodels here and run `yarn redwood prisma migrate dev` +// to create migrations for them and apply to your dev DB. +// TODO: Please remove the following example: +model UserExample { + id Int @id @default(autoincrement()) + email String @unique + name String? +} + +model Post { + id Int @id @default(autoincrement()) + title String + body String + authorId Int + author User @relation(fields: [authorId], references: [id]) + createdAt DateTime @default(now()) +} + +model User { + id Int @id @default(autoincrement()) + email String @unique + hashedPassword String + fullName String + salt String + resetToken String? + resetTokenExpiresAt DateTime? + roles String? + posts Post[] +} + +model Contact { + id Int @id @default(autoincrement()) + name String + email String + message String + createdAt DateTime @default(now()) +} + +model Produce { + id String @id @default(cuid()) + name String @unique + quantity Int + price Int + nutrients String? + region String + /// Available only for fruits + isSeedless Boolean? + /// Available only for fruits + ripenessIndicators String? + /// Available only for vegetables + vegetableFamily String? + /// Available only for vegetables + isPickled Boolean? + stall Stall @relation(fields: [stallId], references: [id], onDelete: Cascade) + stallId String +} + +model Stall { + id String @id @default(cuid()) + name String + stallNumber String @unique + produce Produce[] +} diff --git a/__fixtures__/esm-fragment-test-project/api/package.json b/__fixtures__/esm-fragment-test-project/api/package.json new file mode 100644 index 0000000000..f3b9e4e1cb --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/package.json @@ -0,0 +1,11 @@ +{ + "name": "api", + "type": "module", + "version": "0.0.0", + "private": true, + "dependencies": { + "@cedarjs/api": "0.0.5", + "@cedarjs/auth-dbauth-api": "0.0.5", + "@cedarjs/graphql-server": "0.0.5" + } +} diff --git a/__fixtures__/esm-fragment-test-project/api/src/__tests__/context.test.ts b/__fixtures__/esm-fragment-test-project/api/src/__tests__/context.test.ts new file mode 100644 index 0000000000..972c4756e8 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/__tests__/context.test.ts @@ -0,0 +1,14 @@ +test('Set a mock user on the context', async () => { + const user = { + id: 0o7, + name: 'Bond, James Bond', + email: 'totallyNotASpy@example.com', + roles: 'secret_agent', + } + mockCurrentUser(user) + expect(context.currentUser).toStrictEqual(user) +}) + +test('Context is isolated between tests', () => { + expect(context).toStrictEqual({}) +}) diff --git a/__fixtures__/esm-fragment-test-project/api/src/directives/requireAuth/requireAuth.test.ts b/__fixtures__/esm-fragment-test-project/api/src/directives/requireAuth/requireAuth.test.ts new file mode 100644 index 0000000000..83b683768b --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/directives/requireAuth/requireAuth.test.ts @@ -0,0 +1,20 @@ +import { mockRedwoodDirective, getDirectiveName } from '@cedarjs/testing/api' + +import requireAuth from './requireAuth.js' + +describe('requireAuth directive', () => { + it('declares the directive sdl as schema, with the correct name', () => { + expect(requireAuth.schema).toBeTruthy() + expect(getDirectiveName(requireAuth.schema)).toBe('requireAuth') + }) + + it('requireAuth has stub implementation. Should not throw when current user', () => { + // If you want to set values in context, pass it through e.g. + // mockRedwoodDirective(requireAuth, { context: { currentUser: { id: 1, name: 'Lebron McGretzky' } }}) + const mockExecution = mockRedwoodDirective(requireAuth, { + context: { currentUser: { id: 1, roles: 'ADMIN', email: 'b@zinga.com' } }, + }) + + expect(mockExecution).not.toThrowError() + }) +}) diff --git a/__fixtures__/esm-fragment-test-project/api/src/directives/requireAuth/requireAuth.ts b/__fixtures__/esm-fragment-test-project/api/src/directives/requireAuth/requireAuth.ts new file mode 100644 index 0000000000..3dadf21e68 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/directives/requireAuth/requireAuth.ts @@ -0,0 +1,25 @@ +import { gql } from 'graphql-tag' + +import type { ValidatorDirectiveFunc } from '@cedarjs/graphql-server' +import { createValidatorDirective } from '@cedarjs/graphql-server' + +import { requireAuth as applicationRequireAuth } from 'src/lib/auth.js' + +export const schema = gql` + """ + Use to check whether or not a user is authenticated and is associated + with an optional set of roles. + """ + directive @requireAuth(roles: [String]) on FIELD_DEFINITION +` + +type RequireAuthValidate = ValidatorDirectiveFunc<{ roles?: string[] }> + +const validate: RequireAuthValidate = ({ directiveArgs }) => { + const { roles } = directiveArgs + applicationRequireAuth({ roles }) +} + +const requireAuth = createValidatorDirective(schema, validate) + +export default requireAuth diff --git a/__fixtures__/esm-fragment-test-project/api/src/directives/skipAuth/skipAuth.test.ts b/__fixtures__/esm-fragment-test-project/api/src/directives/skipAuth/skipAuth.test.ts new file mode 100644 index 0000000000..68c006bdae --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/directives/skipAuth/skipAuth.test.ts @@ -0,0 +1,10 @@ +import { getDirectiveName } from '@cedarjs/testing/api' + +import skipAuth from './skipAuth.js' + +describe('skipAuth directive', () => { + it('declares the directive sdl as schema, with the correct name', () => { + expect(skipAuth.schema).toBeTruthy() + expect(getDirectiveName(skipAuth.schema)).toBe('skipAuth') + }) +}) diff --git a/__fixtures__/esm-fragment-test-project/api/src/directives/skipAuth/skipAuth.ts b/__fixtures__/esm-fragment-test-project/api/src/directives/skipAuth/skipAuth.ts new file mode 100644 index 0000000000..fdea5cf17b --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/directives/skipAuth/skipAuth.ts @@ -0,0 +1,16 @@ +import { gql } from 'graphql-tag' + +import { createValidatorDirective } from '@cedarjs/graphql-server' + +export const schema = gql` + """ + Use to skip authentication checks and allow public access. + """ + directive @skipAuth on FIELD_DEFINITION +` + +const skipAuth = createValidatorDirective(schema, () => { + return +}) + +export default skipAuth diff --git a/__fixtures__/esm-fragment-test-project/api/src/functions/auth.ts b/__fixtures__/esm-fragment-test-project/api/src/functions/auth.ts new file mode 100644 index 0000000000..b2192a0dcf --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/functions/auth.ts @@ -0,0 +1,202 @@ +import type { APIGatewayProxyEvent, Context } from 'aws-lambda' + +import { DbAuthHandler } from '@cedarjs/auth-dbauth-api' +import type { DbAuthHandlerOptions, UserType } from '@cedarjs/auth-dbauth-api' + +import { cookieName } from 'src/lib/auth.js' +import { db } from 'src/lib/db.js' + +export const handler = async ( + event: APIGatewayProxyEvent, + context: Context +) => { + const forgotPasswordOptions: DbAuthHandlerOptions['forgotPassword'] = { + // handler() is invoked after verifying that a user was found with the given + // username. This is where you can send the user an email with a link to + // reset their password. With the default dbAuth routes and field names, the + // URL to reset the password will be: + // + // https://example.com/reset-password?resetToken=${user.resetToken} + // + // Whatever is returned from this function will be returned from + // the `forgotPassword()` function that is destructured from `useAuth()`. + // You could use this return value to, for example, show the email + // address in a toast message so the user will know it worked and where + // to look for the email. + // + // Note that this return value is sent to the client in *plain text* + // so don't include anything you wouldn't want prying eyes to see. The + // `user` here has been sanitized to only include the fields listed in + // `allowedUserFields` so it should be safe to return as-is. + handler: (user, _resetToken) => { + // TODO: Send user an email/message with a link to reset their password, + // including the `resetToken`. The URL should look something like: + // `http://localhost:8910/reset-password?resetToken=${resetToken}` + + return user + }, + + // How long the resetToken is valid for, in seconds (default is 24 hours) + expires: 60 * 60 * 24, + + errors: { + // for security reasons you may want to be vague here rather than expose + // the fact that the email address wasn't found (prevents fishing for + // valid email addresses) + usernameNotFound: 'Username not found', + // if the user somehow gets around client validation + usernameRequired: 'Username is required', + }, + } + + const loginOptions: DbAuthHandlerOptions['login'] = { + // handler() is called after finding the user that matches the + // username/password provided at login, but before actually considering them + // logged in. The `user` argument will be the user in the database that + // matched the username/password. + // + // If you want to allow this user to log in simply return the user. + // + // If you want to prevent someone logging in for another reason (maybe they + // didn't validate their email yet), throw an error and it will be returned + // by the `logIn()` function from `useAuth()` in the form of: + // `{ message: 'Error message' }` + handler: (user) => { + return user + }, + + errors: { + usernameOrPasswordMissing: 'Both username and password are required', + usernameNotFound: 'Username ${username} not found', + // For security reasons you may want to make this the same as the + // usernameNotFound error so that a malicious user can't use the error + // to narrow down if it's the username or password that's incorrect + incorrectPassword: 'Incorrect password for ${username}', + }, + + // How long a user will remain logged in, in seconds + expires: 60 * 60 * 24 * 365 * 10, + } + + const resetPasswordOptions: DbAuthHandlerOptions['resetPassword'] = { + // handler() is invoked after the password has been successfully updated in + // the database. Returning anything truthy will automatically log the user + // in. Return `false` otherwise, and in the Reset Password page redirect the + // user to the login page. + handler: (_user) => { + return true + }, + + // If `false` then the new password MUST be different from the current one + allowReusedPassword: true, + + errors: { + // the resetToken is valid, but expired + resetTokenExpired: 'resetToken is expired', + // no user was found with the given resetToken + resetTokenInvalid: 'resetToken is invalid', + // the resetToken was not present in the URL + resetTokenRequired: 'resetToken is required', + // new password is the same as the old password (apparently they did not forget it) + reusedPassword: 'Must choose a new password', + }, + } + + interface UserAttributes { + 'full-name': string + } + + const signupOptions: DbAuthHandlerOptions< + UserType, + UserAttributes + >['signup'] = { + // Whatever you want to happen to your data on new user signup. Redwood will + // check for duplicate usernames before calling this handler. At a minimum + // you need to save the `username`, `hashedPassword` and `salt` to your + // user table. `userAttributes` contains any additional object members that + // were included in the object given to the `signUp()` function you got + // from `useAuth()`. + // + // If you want the user to be immediately logged in, return the user that + // was created. + // + // If this handler throws an error, it will be returned by the `signUp()` + // function in the form of: `{ error: 'Error message' }`. + // + // If this returns anything else, it will be returned by the + // `signUp()` function in the form of: `{ message: 'String here' }`. + handler: ({ username, hashedPassword, salt, userAttributes }) => { + return db.user.create({ + data: { + email: username, + hashedPassword: hashedPassword, + salt: salt, + fullName: userAttributes['full-name'], + }, + }) + }, + + // Include any format checks for password here. Return `true` if the + // password is valid, otherwise throw a `PasswordValidationError`. + // Import the error along with `DbAuthHandler` from `@cedarjs/api` above. + passwordValidation: (_password) => { + return true + }, + + errors: { + // `field` will be either "username" or "password" + fieldMissing: '${field} is required', + usernameTaken: 'Username `${username}` already in use', + }, + } + + const authHandler = new DbAuthHandler(event, context, { + // Provide prisma db client + db: db, + + // The name of the property you'd call on `db` to access your user table. + // i.e. if your Prisma model is named `User` this value would be `user`, as in `db.user` + authModelAccessor: 'user', + + // A map of what dbAuth calls a field to what your database calls it. + // `id` is whatever column you use to uniquely identify a user (probably + // something like `id` or `userId` or even `email`) + authFields: { + id: 'id', + username: 'email', + hashedPassword: 'hashedPassword', + salt: 'salt', + resetToken: 'resetToken', + resetTokenExpiresAt: 'resetTokenExpiresAt', + }, + + // A list of fields on your user object that are safe to return to the + // client when invoking a handler that returns a user (like forgotPassword + // and signup). This list should be as small as possible to be sure not to + // leak any sensitive information to the client. + allowedUserFields: ['id', 'email'], + + // Specifies attributes on the cookie that dbAuth sets in order to remember + // who is logged in. See https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#restrict_access_to_cookies + cookie: { + attributes: { + HttpOnly: true, + Path: '/', + SameSite: 'Lax', + Secure: process.env.NODE_ENV !== 'development', + + // If you need to allow other domains (besides the api side) access to + // the dbAuth session cookie: + // Domain: 'example.com', + }, + name: cookieName, + }, + + forgotPassword: forgotPasswordOptions, + login: loginOptions, + resetPassword: resetPasswordOptions, + signup: signupOptions, + }) + + return await authHandler.invoke() +} diff --git a/__fixtures__/esm-fragment-test-project/api/src/functions/graphql.ts b/__fixtures__/esm-fragment-test-project/api/src/functions/graphql.ts new file mode 100644 index 0000000000..4445ef3976 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/functions/graphql.ts @@ -0,0 +1,25 @@ +import { createAuthDecoder } from '@cedarjs/auth-dbauth-api' +import { createGraphQLHandler } from '@cedarjs/graphql-server' + +import directives from 'src/directives/**/*.{js,ts}' +import sdls from 'src/graphql/**/*.sdl.{js,ts}' +import services from 'src/services/**/*.{js,ts}' + +import { cookieName, getCurrentUser } from 'src/lib/auth.js' +import { db } from 'src/lib/db.js' +import { logger } from 'src/lib/logger.js' + +const authDecoder = createAuthDecoder(cookieName) + +export const handler = createGraphQLHandler({ + authDecoder, + getCurrentUser, + loggerConfig: { logger, options: {} }, + directives, + sdls, + services, + onException: () => { + // Disconnect from your database with an unhandled exception. + db.$disconnect() + }, +}) diff --git a/__fixtures__/esm-fragment-test-project/api/src/graphql/.keep b/__fixtures__/esm-fragment-test-project/api/src/graphql/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/__fixtures__/esm-fragment-test-project/api/src/graphql/contacts.sdl.ts b/__fixtures__/esm-fragment-test-project/api/src/graphql/contacts.sdl.ts new file mode 100644 index 0000000000..7dec262a57 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/graphql/contacts.sdl.ts @@ -0,0 +1,32 @@ +export const schema = gql` + type Contact { + id: Int! + name: String! + email: String! + message: String! + createdAt: DateTime! + } + + type Query { + contacts: [Contact!]! @requireAuth + contact(id: Int!): Contact @requireAuth + } + + input CreateContactInput { + name: String! + email: String! + message: String! + } + + input UpdateContactInput { + name: String + email: String + message: String + } + + type Mutation { + createContact(input: CreateContactInput!): Contact @skipAuth + updateContact(id: Int!, input: UpdateContactInput!): Contact! @requireAuth + deleteContact(id: Int!): Contact! @requireAuth(roles: ["ADMIN"]) + } +` diff --git a/__fixtures__/esm-fragment-test-project/api/src/graphql/groceries.sdl.ts b/__fixtures__/esm-fragment-test-project/api/src/graphql/groceries.sdl.ts new file mode 100644 index 0000000000..0870d7daeb --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/graphql/groceries.sdl.ts @@ -0,0 +1,49 @@ +export const schema = gql` + interface Grocery { + id: ID! + name: String! + quantity: Int! + price: Int! + nutrients: String + stall: Stall! + region: String! + } + + type Fruit implements Grocery { + id: ID! + name: String! + quantity: Int! + price: Int! + nutrients: String + stall: Stall! + region: String! + "Seedless is only for fruits" + isSeedless: Boolean + "Ripeness is only for fruits" + ripenessIndicators: String + } + + type Vegetable implements Grocery { + id: ID! + name: String! + quantity: Int! + price: Int! + nutrients: String + stall: Stall! + region: String! + "Veggie Family is only for vegetables" + vegetableFamily: String + "Pickled is only for vegetables" + isPickled: Boolean + } + + union Groceries = Fruit | Vegetable + + type Query { + groceries: [Groceries!]! @skipAuth + fruits: [Fruit!]! @skipAuth + fruitById(id: ID!): Fruit @skipAuth + vegetables: [Vegetable!]! @skipAuth + vegetableById(id: ID!): Vegetable @skipAuth + } +` diff --git a/__fixtures__/esm-fragment-test-project/api/src/graphql/posts.sdl.ts b/__fixtures__/esm-fragment-test-project/api/src/graphql/posts.sdl.ts new file mode 100644 index 0000000000..09cf9b2cc6 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/graphql/posts.sdl.ts @@ -0,0 +1,33 @@ +export const schema = gql` + type Post { + id: Int! + title: String! + body: String! + authorId: Int! + author: User! + createdAt: DateTime! + } + + type Query { + posts: [Post!]! @skipAuth + post(id: Int!): Post @skipAuth + } + + input CreatePostInput { + title: String! + body: String! + authorId: Int! + } + + input UpdatePostInput { + title: String + body: String + authorId: Int + } + + type Mutation { + createPost(input: CreatePostInput!): Post! @requireAuth + updatePost(id: Int!, input: UpdatePostInput!): Post! @requireAuth + deletePost(id: Int!): Post! @requireAuth + } +` diff --git a/__fixtures__/esm-fragment-test-project/api/src/graphql/produces.sdl.ts b/__fixtures__/esm-fragment-test-project/api/src/graphql/produces.sdl.ts new file mode 100644 index 0000000000..1a3342f662 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/graphql/produces.sdl.ts @@ -0,0 +1,54 @@ +export const schema = gql` + type Produce { + id: String! + name: String! + quantity: Int! + price: Int! + nutrients: String + region: String! + isSeedless: Boolean + ripenessIndicators: String + vegetableFamily: String + isPickled: Boolean + stall: Stall! + stallId: String! + } + + type Query { + produces: [Produce!]! @skipAuth + produce(id: String!): Produce @skipAuth + } + + input CreateProduceInput { + name: String! + quantity: Int! + price: Int! + nutrients: String + region: String! + isSeedless: Boolean + ripenessIndicators: String + vegetableFamily: String + isPickled: Boolean + stallId: String! + } + + input UpdateProduceInput { + name: String + quantity: Int + price: Int + nutrients: String + region: String + isSeedless: Boolean + ripenessIndicators: String + vegetableFamily: String + isPickled: Boolean + stallId: String + } + + type Mutation { + createProduce(input: CreateProduceInput!): Produce! @skipAuth + updateProduce(id: String!, input: UpdateProduceInput!): Produce! + @skipAuth + deleteProduce(id: String!): Produce! @skipAuth + } +` diff --git a/__fixtures__/esm-fragment-test-project/api/src/graphql/stalls.sdl.ts b/__fixtures__/esm-fragment-test-project/api/src/graphql/stalls.sdl.ts new file mode 100644 index 0000000000..c934eeb4a5 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/graphql/stalls.sdl.ts @@ -0,0 +1,29 @@ +export const schema = gql` + type Stall { + id: String! + name: String! + stallNumber: String! + produce: [Produce]! + } + + type Query { + stalls: [Stall!]! @requireAuth + stall(id: String!): Stall @requireAuth + } + + input CreateStallInput { + name: String! + stallNumber: String! + } + + input UpdateStallInput { + name: String + stallNumber: String + } + + type Mutation { + createStall(input: CreateStallInput!): Stall! @requireAuth + updateStall(id: String!, input: UpdateStallInput!): Stall! @requireAuth + deleteStall(id: String!): Stall! @requireAuth + } +` diff --git a/__fixtures__/esm-fragment-test-project/api/src/graphql/users.sdl.ts b/__fixtures__/esm-fragment-test-project/api/src/graphql/users.sdl.ts new file mode 100644 index 0000000000..e2d1c0bed1 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/graphql/users.sdl.ts @@ -0,0 +1,25 @@ +export const schema = gql` + type User { + id: Int! + email: String! + fullName: String! + roles: String + posts: [Post]! + } + + type Query { + user(id: Int!): User @skipAuth + } + + input CreateUserInput { + email: String! + fullName: String! + roles: String + } + + input UpdateUserInput { + email: String + fullName: String + roles: String + } +` diff --git a/__fixtures__/esm-fragment-test-project/api/src/lib/auth.ts b/__fixtures__/esm-fragment-test-project/api/src/lib/auth.ts new file mode 100644 index 0000000000..e7eafb1ef6 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/lib/auth.ts @@ -0,0 +1,121 @@ +import type { Decoded } from '@cedarjs/api' +import { AuthenticationError, ForbiddenError } from '@cedarjs/graphql-server' + +import { db } from './db.js' + +/** + * The name of the cookie that dbAuth sets + * + * %port% will be replaced with the port the api server is running on. + * If you have multiple RW apps running on the same host, you'll need to + * make sure they all use unique cookie names + */ +export const cookieName = 'session_%port%' + +/** + * The session object sent in as the first argument to getCurrentUser() will + * have a single key `id` containing the unique ID of the logged in user + * (whatever field you set as `authFields.id` in your auth function config). + * You'll need to update the call to `db` below if you use a different model + * name or unique field name, for example: + * + * return await db.profile.findUnique({ where: { email: session.id } }) + * ───┬─── ──┬── + * model accessor ─┘ unique id field name ─┘ + * + * !! BEWARE !! Anything returned from this function will be available to the + * client--it becomes the content of `currentUser` on the web side (as well as + * `context.currentUser` on the api side). You should carefully add additional + * fields to the `select` object below once you've decided they are safe to be + * seen if someone were to open the Web Inspector in their browser. + */ +export const getCurrentUser = async (session: Decoded) => { + if (!session || typeof session.id !== 'number') { + throw new Error('Invalid session') + } + + return await db.user.findUnique({ + where: { id: session.id }, + select: { id: true, roles: true, email: true }, + }) +} + +/** + * The user is authenticated if there is a currentUser in the context + * + * @returns {boolean} - If the currentUser is authenticated + */ +export const isAuthenticated = (): boolean => { + return !!context.currentUser +} + +/** + * When checking role membership, roles can be a single value, a list, or none. + * You can use Prisma enums too (if you're using them for roles), just import your enum type from `@prisma/client` + */ +type AllowedRoles = string | string[] | undefined + +/** + * Checks if the currentUser is authenticated (and assigned one of the given roles) + * + * @param roles: {@link AllowedRoles} - Checks if the currentUser is assigned one of these roles + * + * @returns {boolean} - Returns true if the currentUser is logged in and assigned one of the given roles, + * or when no roles are provided to check against. Otherwise returns false. + */ +export const hasRole = (roles: AllowedRoles): boolean => { + if (!isAuthenticated()) { + return false + } + + const currentUserRoles = context.currentUser?.roles as string | string[] + + if (typeof roles === 'string') { + if (typeof currentUserRoles === 'string') { + // roles to check is a string, currentUser.roles is a string + return currentUserRoles === roles + } else if (Array.isArray(currentUserRoles)) { + // roles to check is a string, currentUser.roles is an array + return currentUserRoles?.some((allowedRole) => roles === allowedRole) + } + } + + if (Array.isArray(roles)) { + if (Array.isArray(currentUserRoles)) { + // roles to check is an array, currentUser.roles is an array + return currentUserRoles?.some((allowedRole) => + roles.includes(allowedRole) + ) + } else if (typeof currentUserRoles === 'string') { + // roles to check is an array, currentUser.roles is a string + return roles.some((allowedRole) => currentUserRoles === allowedRole) + } + } + + // roles not found + return false +} + +/** + * Use requireAuth in your services to check that a user is logged in, + * whether or not they are assigned a role, and optionally raise an + * error if they're not. + * + * @param roles: {@link AllowedRoles} - When checking role membership, these roles grant access. + * + * @returns - If the currentUser is authenticated (and assigned one of the given roles) + * + * @throws {@link AuthenticationError} - If the currentUser is not authenticated + * @throws {@link ForbiddenError} If the currentUser is not allowed due to role permissions + * + * @see https://github.com/cedarjs/cedar/tree/main/packages/auth for examples + */ +export const requireAuth = ({ roles }: { roles?: AllowedRoles } = {}) => { + if (!isAuthenticated()) { + throw new AuthenticationError("You don't have permission to do that.") + } + + if (roles && !hasRole(roles)) { + throw new ForbiddenError("You don't have access to do that.") + } +} diff --git a/__fixtures__/esm-fragment-test-project/api/src/lib/db.ts b/__fixtures__/esm-fragment-test-project/api/src/lib/db.ts new file mode 100644 index 0000000000..b6fa93228f --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/lib/db.ts @@ -0,0 +1,26 @@ +// See https://www.prisma.io/docs/reference/tools-and-interfaces/prisma-client/constructor +// for options. + +import { PrismaClient } from '@prisma/client' + +import { emitLogLevels, handlePrismaLogging } from '@cedarjs/api/logger' + +import { logger } from './logger.js' + +const prismaClient = new PrismaClient({ + log: emitLogLevels(['info', 'warn', 'error']), +}) + +handlePrismaLogging({ + db: prismaClient, + logger, + logLevels: ['info', 'warn', 'error'], +}) + +/** + * Global Prisma client extensions should be added here, as $extend + * returns a new instance. + * export const db = prismaClient.$extend(...) + * Add any .$on hooks before using $extend + */ +export const db = prismaClient diff --git a/__fixtures__/esm-fragment-test-project/api/src/lib/logger.ts b/__fixtures__/esm-fragment-test-project/api/src/lib/logger.ts new file mode 100644 index 0000000000..b5f38c1921 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/lib/logger.ts @@ -0,0 +1,17 @@ +import { createLogger } from '@cedarjs/api/logger' + +/** + * Creates a logger with RedwoodLoggerOptions + * + * These extend and override default LoggerOptions, + * can define a destination like a file or other supported pino log transport stream, + * and sets whether or not to show the logger configuration settings (defaults to false) + * + * @param RedwoodLoggerOptions + * + * RedwoodLoggerOptions have + * @param {options} LoggerOptions - defines how to log, such as redaction and format + * @param {string | DestinationStream} destination - defines where to log, such as a transport stream or file + * @param {boolean} showConfig - whether to display logger configuration on initialization + */ +export const logger = createLogger({}) diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/.keep b/__fixtures__/esm-fragment-test-project/api/src/services/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/contacts/contacts.scenarios.ts b/__fixtures__/esm-fragment-test-project/api/src/services/contacts/contacts.scenarios.ts new file mode 100644 index 0000000000..99271967d8 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/services/contacts/contacts.scenarios.ts @@ -0,0 +1,12 @@ +import type { Prisma, Contact } from '@prisma/client' + +import type { ScenarioData } from '@cedarjs/testing/api' + +export const standard = defineScenario({ + contact: { + one: { data: { name: 'String', email: 'String', message: 'String' } }, + two: { data: { name: 'String', email: 'String', message: 'String' } }, + }, +}) + +export type StandardScenario = ScenarioData diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/contacts/contacts.test.ts b/__fixtures__/esm-fragment-test-project/api/src/services/contacts/contacts.test.ts new file mode 100644 index 0000000000..bec7102a29 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/services/contacts/contacts.test.ts @@ -0,0 +1,59 @@ +import type { Contact } from '@prisma/client' + +import { + contacts, + contact, + createContact, + updateContact, + deleteContact, +} from './contacts.js' +import type { StandardScenario } from './contacts.scenarios.js' + +// Generated boilerplate tests do not account for all circumstances +// and can fail without adjustments, e.g. Float. +// Please refer to the RedwoodJS Testing Docs: +// https://redwoodjs.com/docs/testing#testing-services +// https://redwoodjs.com/docs/testing#jest-expect-type-considerations + +describe('contacts', () => { + scenario('returns all contacts', async (scenario: StandardScenario) => { + const result = await contacts() + + expect(result.length).toEqual(Object.keys(scenario.contact).length) + }) + + scenario('returns a single contact', async (scenario: StandardScenario) => { + const result = await contact({ id: scenario.contact.one.id }) + + expect(result).toEqual(scenario.contact.one) + }) + + scenario('creates a contact', async () => { + const result = await createContact({ + input: { name: 'String', email: 'String', message: 'String' }, + }) + + expect(result.name).toEqual('String') + expect(result.email).toEqual('String') + expect(result.message).toEqual('String') + }) + + scenario('updates a contact', async (scenario: StandardScenario) => { + const original = (await contact({ id: scenario.contact.one.id })) as Contact + const result = await updateContact({ + id: original.id, + input: { name: 'String2' }, + }) + + expect(result.name).toEqual('String2') + }) + + scenario('deletes a contact', async (scenario: StandardScenario) => { + const original = (await deleteContact({ + id: scenario.contact.one.id, + })) as Contact + const result = await contact({ id: original.id }) + + expect(result).toEqual(null) + }) +}) diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/contacts/contacts.ts b/__fixtures__/esm-fragment-test-project/api/src/services/contacts/contacts.ts new file mode 100644 index 0000000000..88b001d48a --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/services/contacts/contacts.ts @@ -0,0 +1,37 @@ +import type { QueryResolvers, MutationResolvers } from 'types/graphql.js' + +import { db } from 'src/lib/db.js' + +export const contacts: QueryResolvers['contacts'] = () => { + return db.contact.findMany() +} + +export const contact: QueryResolvers['contact'] = ({ id }) => { + return db.contact.findUnique({ + where: { id }, + }) +} + +export const createContact: MutationResolvers['createContact'] = ({ + input, +}) => { + return db.contact.create({ + data: input, + }) +} + +export const updateContact: MutationResolvers['updateContact'] = ({ + id, + input, +}) => { + return db.contact.update({ + data: input, + where: { id }, + }) +} + +export const deleteContact: MutationResolvers['deleteContact'] = ({ id }) => { + return db.contact.delete({ + where: { id }, + }) +} diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/contacts/describeContacts.scenarios.ts b/__fixtures__/esm-fragment-test-project/api/src/services/contacts/describeContacts.scenarios.ts new file mode 100644 index 0000000000..99271967d8 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/services/contacts/describeContacts.scenarios.ts @@ -0,0 +1,12 @@ +import type { Prisma, Contact } from '@prisma/client' + +import type { ScenarioData } from '@cedarjs/testing/api' + +export const standard = defineScenario({ + contact: { + one: { data: { name: 'String', email: 'String', message: 'String' } }, + two: { data: { name: 'String', email: 'String', message: 'String' } }, + }, +}) + +export type StandardScenario = ScenarioData diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/contacts/describeContacts.test.ts b/__fixtures__/esm-fragment-test-project/api/src/services/contacts/describeContacts.test.ts new file mode 100644 index 0000000000..ad9a697c07 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/services/contacts/describeContacts.test.ts @@ -0,0 +1,57 @@ +import { db } from 'src/lib/db.js' + +import { contact, contacts, createContact } from './contacts.js' +import type { StandardScenario } from './contacts.scenarios.js' + +/** + * Example test for describe scenario. + * + * Note that scenario tests need a matching [name].scenarios.ts file. + */ + +describeScenario('contacts', (getScenario) => { + let scenario: StandardScenario + + beforeEach(() => { + scenario = getScenario() + }) + + it('returns all contacts', async () => { + const result = await contacts() + + expect(result.length).toEqual(Object.keys(scenario.contact).length) + }) + + it('returns a single contact', async () => { + const result = await contact({ id: scenario.contact.one.id }) + + expect(result).toEqual(scenario.contact.one) + }) + + it('creates a contact', async () => { + const result = await createContact({ + input: { + name: 'Bazinga', + email: 'contact@describe.scenario', + message: 'Describe scenario works!', + }, + }) + + expect(result.name).toEqual('Bazinga') + expect(result.email).toEqual('contact@describe.scenario') + expect(result.message).toEqual('Describe scenario works!') + }) + + it('Checking that describe scenario works', async () => { + // This test is dependent on the above test. If you used a normal scenario it would not work + const contactCreatedInAboveTest = await db.contact.findFirst({ + where: { + email: 'contact@describe.scenario', + }, + }) + + expect(contactCreatedInAboveTest.message).toEqual( + 'Describe scenario works!' + ) + }) +}) diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/groceries.ts b/__fixtures__/esm-fragment-test-project/api/src/services/groceries.ts new file mode 100644 index 0000000000..fbfa9ac9f2 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/services/groceries.ts @@ -0,0 +1,32 @@ +import type { Produce } from 'types/graphql.js' + +import { db } from 'src/lib/db.js' + +const isFruit = (grocery: Produce) => { + return grocery.isSeedless !== null && grocery.ripenessIndicators !== null +} + +export const groceries = async () => { + const result = await db.produce.findMany({ + include: { stall: true }, + orderBy: { name: 'asc' }, + }) + + const avail = result.map((grocery) => { + if (isFruit(grocery)) { + return { + ...grocery, + __typename: 'Fruit', + __resolveType: 'Fruit', + } + } else { + return { + ...grocery, + __typename: 'Vegetable', + __resolveType: 'Vegetable', + } + } + }) + + return avail +} diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/posts/posts.scenarios.ts b/__fixtures__/esm-fragment-test-project/api/src/services/posts/posts.scenarios.ts new file mode 100644 index 0000000000..6890e49ad1 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/services/posts/posts.scenarios.ts @@ -0,0 +1,38 @@ +import type { Prisma, Post } from '@prisma/client' + +import type { ScenarioData } from '@cedarjs/testing/api' + +export const standard = defineScenario({ + post: { + one: { + data: { + title: 'String', + body: 'String', + author: { + create: { + email: 'String13', + hashedPassword: 'String', + fullName: 'String', + salt: 'String', + }, + }, + }, + }, + two: { + data: { + title: 'String', + body: 'String', + author: { + create: { + email: 'String27', + hashedPassword: 'String', + fullName: 'String', + salt: 'String', + }, + }, + }, + }, + }, +}) + +export type StandardScenario = ScenarioData diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/posts/posts.test.ts b/__fixtures__/esm-fragment-test-project/api/src/services/posts/posts.test.ts new file mode 100644 index 0000000000..b7c7572341 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/services/posts/posts.test.ts @@ -0,0 +1,55 @@ +import type { Post } from '@prisma/client' + +import { posts, post, createPost, updatePost, deletePost } from './posts.js' +import type { StandardScenario } from './posts.scenarios.js' + +// Generated boilerplate tests do not account for all circumstances +// and can fail without adjustments, e.g. Float. +// Please refer to the RedwoodJS Testing Docs: +// https://redwoodjs.com/docs/testing#testing-services +// https://redwoodjs.com/docs/testing#jest-expect-type-considerations + +describe('posts', () => { + scenario('returns all posts', async (scenario: StandardScenario) => { + const result = await posts() + + expect(result.length).toEqual(Object.keys(scenario.post).length) + }) + + scenario('returns a single post', async (scenario: StandardScenario) => { + const result = await post({ id: scenario.post.one.id }) + + expect(result).toEqual(scenario.post.one) + }) + + scenario('creates a post', async (scenario: StandardScenario) => { + const result = await createPost({ + input: { + title: 'String', + body: 'String', + authorId: scenario.post.two.authorId, + }, + }) + + expect(result.title).toEqual('String') + expect(result.body).toEqual('String') + expect(result.authorId).toEqual(scenario.post.two.authorId) + }) + + scenario('updates a post', async (scenario: StandardScenario) => { + const original = (await post({ id: scenario.post.one.id })) as Post + const result = await updatePost({ + id: original.id, + input: { title: 'String2' }, + }) + + expect(result.title).toEqual('String2') + }) + + scenario('deletes a post', async (scenario: StandardScenario) => { + const original = (await deletePost({ id: scenario.post.one.id })) as Post + const result = await post({ id: original.id }) + + expect(result).toEqual(null) + }) +}) diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/posts/posts.ts b/__fixtures__/esm-fragment-test-project/api/src/services/posts/posts.ts new file mode 100644 index 0000000000..afbc87a734 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/services/posts/posts.ts @@ -0,0 +1,42 @@ +import type { + QueryResolvers, + MutationResolvers, + PostRelationResolvers, +} from 'types/graphql.js' + +import { db } from 'src/lib/db.js' + +export const posts: QueryResolvers['posts'] = () => { + return db.post.findMany() +} + +export const post: QueryResolvers['post'] = ({ id }) => { + return db.post.findUnique({ + where: { id }, + }) +} + +export const createPost: MutationResolvers['createPost'] = ({ input }) => { + return db.post.create({ + data: input, + }) +} + +export const updatePost: MutationResolvers['updatePost'] = ({ id, input }) => { + return db.post.update({ + data: input, + where: { id }, + }) +} + +export const deletePost: MutationResolvers['deletePost'] = ({ id }) => { + return db.post.delete({ + where: { id }, + }) +} + +export const Post: PostRelationResolvers = { + author: (_obj, { root }) => { + return db.post.findUnique({ where: { id: root?.id } }).author() + }, +} diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/produces/produces.scenarios.ts b/__fixtures__/esm-fragment-test-project/api/src/services/produces/produces.scenarios.ts new file mode 100644 index 0000000000..390a0fef2d --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/services/produces/produces.scenarios.ts @@ -0,0 +1,28 @@ +import type { Prisma, Produce } from '@prisma/client' + +import type { ScenarioData } from '@cedarjs/testing/api' + +export const standard = defineScenario({ + produce: { + one: { + data: { + name: 'String2231134', + quantity: 6978607, + price: 9949534, + region: 'String', + stall: { create: { name: 'String', stallNumber: 'String9443378' } }, + }, + }, + two: { + data: { + name: 'String5325933', + quantity: 9629727, + price: 360315, + region: 'String', + stall: { create: { name: 'String', stallNumber: 'String8448512' } }, + }, + }, + }, +}) + +export type StandardScenario = ScenarioData diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/produces/produces.test.ts b/__fixtures__/esm-fragment-test-project/api/src/services/produces/produces.test.ts new file mode 100644 index 0000000000..e58c263f08 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/services/produces/produces.test.ts @@ -0,0 +1,67 @@ +import type { Produce } from '@prisma/client' + +import { + produces, + produce, + createProduce, + updateProduce, + deleteProduce, +} from './produces.js' +import type { StandardScenario } from './produces.scenarios.js' + +// Generated boilerplate tests do not account for all circumstances +// and can fail without adjustments, e.g. Float. +// Please refer to the RedwoodJS Testing Docs: +// https://redwoodjs.com/docs/testing#testing-services +// https://redwoodjs.com/docs/testing#jest-expect-type-considerations + +describe('produces', () => { + scenario('returns all produces', async (scenario: StandardScenario) => { + const result = await produces() + + expect(result.length).toEqual(Object.keys(scenario.produce).length) + }) + + scenario('returns a single produce', async (scenario: StandardScenario) => { + const result = await produce({ id: scenario.produce.one.id }) + + expect(result).toEqual(scenario.produce.one) + }) + + scenario('creates a produce', async (scenario: StandardScenario) => { + const result = await createProduce({ + input: { + name: 'String4747749', + quantity: 9239113, + price: 1579575, + region: 'String', + stallId: scenario.produce.two.stallId, + }, + }) + + expect(result.name).toEqual('String4747749') + expect(result.quantity).toEqual(9239113) + expect(result.price).toEqual(1579575) + expect(result.region).toEqual('String') + expect(result.stallId).toEqual(scenario.produce.two.stallId) + }) + + scenario('updates a produce', async (scenario: StandardScenario) => { + const original = (await produce({ id: scenario.produce.one.id })) as Produce + const result = await updateProduce({ + id: original.id, + input: { name: 'String9726252' }, + }) + + expect(result.name).toEqual('String9726252') + }) + + scenario('deletes a produce', async (scenario: StandardScenario) => { + const original = (await deleteProduce({ + id: scenario.produce.one.id, + })) as Produce + const result = await produce({ id: original.id }) + + expect(result).toEqual(null) + }) +}) diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/produces/produces.ts b/__fixtures__/esm-fragment-test-project/api/src/services/produces/produces.ts new file mode 100644 index 0000000000..40f84d949c --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/services/produces/produces.ts @@ -0,0 +1,47 @@ +import type { + QueryResolvers, + MutationResolvers, + ProduceRelationResolvers, +} from 'types/graphql.js' + +import { db } from 'src/lib/db.js' + +export const produces: QueryResolvers['produces'] = () => { + return db.produce.findMany() +} + +export const produce: QueryResolvers['produce'] = ({ id }) => { + return db.produce.findUnique({ + where: { id }, + }) +} + +export const createProduce: MutationResolvers['createProduce'] = ({ + input, +}) => { + return db.produce.create({ + data: input, + }) +} + +export const updateProduce: MutationResolvers['updateProduce'] = ({ + id, + input, +}) => { + return db.produce.update({ + data: input, + where: { id }, + }) +} + +export const deleteProduce: MutationResolvers['deleteProduce'] = ({ id }) => { + return db.produce.delete({ + where: { id }, + }) +} + +export const Produce: ProduceRelationResolvers = { + stall: (_obj, { root }) => { + return db.produce.findUnique({ where: { id: root?.id } }).stall() + }, +} diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/stalls/stalls.scenarios.ts b/__fixtures__/esm-fragment-test-project/api/src/services/stalls/stalls.scenarios.ts new file mode 100644 index 0000000000..1e41eda101 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/services/stalls/stalls.scenarios.ts @@ -0,0 +1,12 @@ +import type { Prisma, Stall } from '@prisma/client' + +import type { ScenarioData } from '@cedarjs/testing/api' + +export const standard = defineScenario({ + stall: { + one: { data: { name: 'String', stallNumber: 'String9136837' } }, + two: { data: { name: 'String', stallNumber: 'String9074776' } }, + }, +}) + +export type StandardScenario = ScenarioData diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/stalls/stalls.test.ts b/__fixtures__/esm-fragment-test-project/api/src/services/stalls/stalls.test.ts new file mode 100644 index 0000000000..6956405eba --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/services/stalls/stalls.test.ts @@ -0,0 +1,56 @@ +import type { Stall } from '@prisma/client' + +import { + stalls, + stall, + createStall, + updateStall, + deleteStall, +} from './stalls.js' +import type { StandardScenario } from './stalls.scenarios.js' + +// Generated boilerplate tests do not account for all circumstances +// and can fail without adjustments, e.g. Float. +// Please refer to the RedwoodJS Testing Docs: +// https://redwoodjs.com/docs/testing#testing-services +// https://redwoodjs.com/docs/testing#jest-expect-type-considerations + +describe('stalls', () => { + scenario('returns all stalls', async (scenario: StandardScenario) => { + const result = await stalls() + + expect(result.length).toEqual(Object.keys(scenario.stall).length) + }) + + scenario('returns a single stall', async (scenario: StandardScenario) => { + const result = await stall({ id: scenario.stall.one.id }) + + expect(result).toEqual(scenario.stall.one) + }) + + scenario('creates a stall', async () => { + const result = await createStall({ + input: { name: 'String', stallNumber: 'String1516174' }, + }) + + expect(result.name).toEqual('String') + expect(result.stallNumber).toEqual('String1516174') + }) + + scenario('updates a stall', async (scenario: StandardScenario) => { + const original = (await stall({ id: scenario.stall.one.id })) as Stall + const result = await updateStall({ + id: original.id, + input: { name: 'String2' }, + }) + + expect(result.name).toEqual('String2') + }) + + scenario('deletes a stall', async (scenario: StandardScenario) => { + const original = (await deleteStall({ id: scenario.stall.one.id })) as Stall + const result = await stall({ id: original.id }) + + expect(result).toEqual(null) + }) +}) diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/stalls/stalls.ts b/__fixtures__/esm-fragment-test-project/api/src/services/stalls/stalls.ts new file mode 100644 index 0000000000..7d34a5eed2 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/services/stalls/stalls.ts @@ -0,0 +1,45 @@ +import type { + QueryResolvers, + MutationResolvers, + StallRelationResolvers, +} from 'types/graphql.js' + +import { db } from 'src/lib/db.js' + +export const stalls: QueryResolvers['stalls'] = () => { + return db.stall.findMany() +} + +export const stall: QueryResolvers['stall'] = ({ id }) => { + return db.stall.findUnique({ + where: { id }, + }) +} + +export const createStall: MutationResolvers['createStall'] = ({ input }) => { + return db.stall.create({ + data: input, + }) +} + +export const updateStall: MutationResolvers['updateStall'] = ({ + id, + input, +}) => { + return db.stall.update({ + data: input, + where: { id }, + }) +} + +export const deleteStall: MutationResolvers['deleteStall'] = ({ id }) => { + return db.stall.delete({ + where: { id }, + }) +} + +export const Stall: StallRelationResolvers = { + produce: (_obj, { root }) => { + return db.stall.findUnique({ where: { id: root?.id } }).produce() + }, +} diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/users/users.scenarios.ts b/__fixtures__/esm-fragment-test-project/api/src/services/users/users.scenarios.ts new file mode 100644 index 0000000000..159e3cabd9 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/services/users/users.scenarios.ts @@ -0,0 +1,26 @@ +import type { Prisma, User } from '@prisma/client' + +import type { ScenarioData } from '@cedarjs/testing/api' + +export const standard = defineScenario({ + user: { + one: { + data: { + email: 'String9', + hashedPassword: 'String', + fullName: 'String', + salt: 'String', + }, + }, + two: { + data: { + email: 'String17', + hashedPassword: 'String', + fullName: 'String', + salt: 'String', + }, + }, + }, +}) + +export type StandardScenario = ScenarioData diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/users/users.test.ts b/__fixtures__/esm-fragment-test-project/api/src/services/users/users.test.ts new file mode 100644 index 0000000000..f754f23d1d --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/services/users/users.test.ts @@ -0,0 +1,10 @@ +import { user } from './users.js' +import type { StandardScenario } from './users.scenarios.js' + +describe('users', () => { + scenario('returns a single user', async (scenario: StandardScenario) => { + const result = await user({ id: scenario.user.one.id }) + + expect(result).toEqual(scenario.user.one) + }) +}) diff --git a/__fixtures__/esm-fragment-test-project/api/src/services/users/users.ts b/__fixtures__/esm-fragment-test-project/api/src/services/users/users.ts new file mode 100644 index 0000000000..5aeb1f4e53 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/src/services/users/users.ts @@ -0,0 +1,17 @@ +import type { QueryResolvers, UserRelationResolvers } from 'types/graphql.js' + +import { db } from 'src/lib/db.js' + +export {} + +export const user: QueryResolvers['user'] = ({ id }) => { + return db.user.findUnique({ + where: { id }, + }) +} + +export const User: UserRelationResolvers = { + posts: (_obj, { root }) => { + return db.user.findUnique({ where: { id: root?.id } }).posts() + }, +} diff --git a/__fixtures__/esm-fragment-test-project/api/tsconfig.json b/__fixtures__/esm-fragment-test-project/api/tsconfig.json new file mode 100644 index 0000000000..c9075d6fac --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "noEmit": true, + "allowJs": true, + "esModuleInterop": true, + "target": "ES2023", + "module": "Node16", + "moduleResolution": "Node16", + "skipLibCheck": false, + "rootDirs": ["./src", "../.redwood/types/mirror/api/src"], + "paths": { + "src/*": ["./src/*", "../.redwood/types/mirror/api/src/*"], + "types/*": ["./types/*", "../types/*"], + "@cedarjs/testing": ["../node_modules/@cedarjs/testing/api"] + }, + "typeRoots": ["../node_modules/@types", "./node_modules/@types"], + "types": ["jest"], + "jsx": "react-jsx" + }, + "include": [ + "src", + "../.redwood/types/includes/all-*", + "../.redwood/types/includes/api-*", + "../types" + ] +} diff --git a/__fixtures__/esm-fragment-test-project/api/vitest.config.ts b/__fixtures__/esm-fragment-test-project/api/vitest.config.ts new file mode 100644 index 0000000000..f3de348f75 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/api/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vitest/config' + +import { cedarVitestPreset } from '@cedarjs/vite/api' + +export default defineConfig({ + plugins: [cedarVitestPreset()], + test: { + globals: true, + }, +}) diff --git a/__fixtures__/esm-fragment-test-project/graphql.config.cjs b/__fixtures__/esm-fragment-test-project/graphql.config.cjs new file mode 100644 index 0000000000..d82bff34e0 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/graphql.config.cjs @@ -0,0 +1,11 @@ +// This file is used by the VSCode GraphQL extension + +const { getPaths } = require('@cedarjs/project-config') + +/** @type {import('graphql-config').IGraphQLConfig} */ +const config = { + schema: getPaths().generated.schema, + documents: './web/src/**/!(*.d).{ts,tsx,js,jsx}', +} + +module.exports = config diff --git a/__fixtures__/esm-fragment-test-project/package.json b/__fixtures__/esm-fragment-test-project/package.json new file mode 100644 index 0000000000..e403c327ae --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/package.json @@ -0,0 +1,32 @@ +{ + "private": true, + "type": "module", + "workspaces": { + "packages": [ + "api", + "web" + ] + }, + "devDependencies": { + "@cedarjs/core": "0.0.5", + "@cedarjs/project-config": "0.0.5", + "@cedarjs/testing": "0.0.5", + "vitest": "3.2.4" + }, + "eslintConfig": { + "extends": "@cedarjs/eslint-config", + "root": true + }, + "engines": { + "node": "=20.x" + }, + "prisma": { + "seed": "yarn rw exec seed" + }, + "packageManager": "yarn@4.9.2", + "resolutions": { + "@storybook/react-dom-shim@npm:7.6.20": "https://verdaccio.tobbe.dev/@storybook/react-dom-shim/-/react-dom-shim-8.0.8.tgz", + "react-is": "19.0.0-rc-f2df5694-20240916", + "vite": "5.4.16" + } +} diff --git a/__fixtures__/esm-fragment-test-project/prettier.config.cjs b/__fixtures__/esm-fragment-test-project/prettier.config.cjs new file mode 100644 index 0000000000..9d81988854 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/prettier.config.cjs @@ -0,0 +1,20 @@ +// https://prettier.io/docs/en/options.html +/** @type {import('prettier').RequiredOptions} */ +module.exports = { + trailingComma: 'es5', + bracketSpacing: true, + tabWidth: 2, + semi: false, + singleQuote: true, + arrowParens: 'always', + overrides: [ + { + files: 'Routes.*', + options: { + printWidth: 999, + }, + }, + ], + tailwindConfig: './web/config/tailwind.config.cjs', + plugins: ['prettier-plugin-tailwindcss'], +} diff --git a/__fixtures__/esm-fragment-test-project/redwood.toml b/__fixtures__/esm-fragment-test-project/redwood.toml new file mode 100644 index 0000000000..42d996a73f --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/redwood.toml @@ -0,0 +1,24 @@ +# This file contains the configuration settings for your Redwood app. +# This file is also what makes your Redwood app a Redwood app. +# If you remove it and try to run `yarn rw dev`, you'll get an error. +# +# For the full list of options, see the "App Configuration: redwood.toml" doc: +# https://redwoodjs.com/docs/app-configuration-redwood-toml + +[web] + title = "Cedar App" + port = "${WEB_DEV_PORT:8910}" + apiUrl = "/.redwood/functions" + includeEnvironmentVariables = [ + # Add any ENV vars that should be available to the web side to this array + # See https://redwoodjs.com/docs/environment-variables#web + ] +[api] + port = "${API_DEV_PORT:8911}" +[browser] + open = true +[notifications] + versionUpdates = ["latest"] + +[graphql] + fragments = true diff --git a/__fixtures__/esm-fragment-test-project/scripts/.keep b/__fixtures__/esm-fragment-test-project/scripts/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/__fixtures__/esm-fragment-test-project/scripts/seed.ts b/__fixtures__/esm-fragment-test-project/scripts/seed.ts new file mode 100644 index 0000000000..ffdb815a6d --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/scripts/seed.ts @@ -0,0 +1,159 @@ +import { db } from 'api/src/lib/db.js' + +// Manually apply seeds via the `yarn rw prisma db seed` command. +// +// Seeds automatically run the first time you run the `yarn rw prisma migrate dev` +// command and every time you run the `yarn rw prisma migrate reset` command. +// +// See https://redwoodjs.com/docs/database-seeds for more info + +export default async () => { + try { + const users = [ + { + id: 1, + email: 'user.one@example.com', + hashedPassword: 'fake_hash', + fullName: 'User One', + salt: 'fake_salt', + }, + { + id: 2, + email: 'user.two@example.com', + hashedPassword: 'fake_hash', + fullName: 'User Two', + salt: 'fake_salt', + }, + ] + + if ((await db.user.count()) === 0) { + await Promise.all(users.map((user) => db.user.create({ data: user }))) + } else { + console.log('Users already seeded') + } + } catch (error) { + console.error(error) + } + + try { + const posts = [ + { + title: 'Welcome to the blog!', + body: "I'm baby single- origin coffee kickstarter lo - fi paleo skateboard.Tumblr hashtag austin whatever DIY plaid knausgaard fanny pack messenger bag blog next level woke.Ethical bitters fixie freegan,helvetica pitchfork 90's tbh chillwave mustache godard subway tile ramps art party. Hammock sustainable twee yr bushwick disrupt unicorn, before they sold out direct trade chicharrones etsy polaroid hoodie. Gentrify offal hoodie fingerstache.", + authorId: 1, + }, + { + title: 'A little more about me', + body: "Raclette shoreditch before they sold out lyft. Ethical bicycle rights meh prism twee. Tote bag ennui vice, slow-carb taiyaki crucifix whatever you probably haven't heard of them jianbing raw denim DIY hot chicken. Chillwave blog succulents freegan synth af ramps poutine wayfarers yr seitan roof party squid. Jianbing flexitarian gentrify hexagon portland single-origin coffee raclette gluten-free. Coloring book cloud bread street art kitsch lumbersexual af distillery ethical ugh thundercats roof party poke chillwave. 90's palo santo green juice subway tile, prism viral butcher selvage etsy pitchfork sriracha tumeric bushwick.", + authorId: 1, + }, + { + title: 'What is the meaning of life?', + body: 'Meh waistcoat succulents umami asymmetrical, hoodie post-ironic paleo chillwave tote bag. Trust fund kitsch waistcoat vape, cray offal gochujang food truck cloud bread enamel pin forage. Roof party chambray ugh occupy fam stumptown. Dreamcatcher tousled snackwave, typewriter lyft unicorn pabst portland blue bottle locavore squid PBR&B tattooed.', + authorId: 2, + }, + ] + + if ((await db.post.count()) === 0) { + await Promise.all( + posts.map(async (post) => { + const newPost = await db.post.create({ data: post }) + + console.log(newPost) + }) + ) + } else { + console.log('Posts already seeded') + } + } catch (error) { + console.error(error) + } + + try { + const stalls = [ + { + id: 'clr0zv6ow000012nvo6r09vog', + name: 'Salad Veggies', + stallNumber: '1', + }, + { + id: 'clr0zvne2000112nvyhzf1ifk', + name: 'Pie Veggies', + stallNumber: '2', + }, + { + id: 'clr0zvne3000212nv6boae9qw', + name: 'Root Veggies', + stallNumber: '3', + }, + ] + + if ((await db.stall.count()) === 0) { + await Promise.all( + stalls.map(async (stall) => { + const newStall = await db.stall.create({ data: stall }) + + console.log(newStall) + }) + ) + } else { + console.log('Stalls already seeded') + } + + const produce = [ + { + id: 'clr0zwyoq000312nvfsu1efcw', + name: 'Lettuce', + quantity: 10, + price: 2, + ripenessIndicators: null, + region: '', + isSeedless: false, + vegetableFamily: 'Asteraceae', + stallId: 'clr0zv6ow000012nvo6r09vog', + }, + { + id: 'clr0zy32x000412nvsya5g8q0', + name: 'Strawberries', + quantity: 24, + price: 3, + ripenessIndicators: 'Vitamin C', + region: 'California', + isSeedless: false, + vegetableFamily: 'Soft', + stallId: 'clr0zvne2000112nvyhzf1ifk', + }, + ] + + if ((await db.produce.count()) === 0) { + await Promise.all( + produce.map(async (produce) => { + const newProduce = await db.produce.create({ data: produce }) + + console.log(newProduce) + }) + ) + } else { + console.log('Produce already seeded') + } + } catch (error) { + console.error(error) + } + + try { + // Create your database records here! For example, seed some users: + // + // const users = [ + // { name: 'Alice', email: 'alice@cedarjs.com' }, + // { name: 'Bob', email: 'bob@cedarjs.com' }, + // ] + // + // await db.user.createMany({ data: users }) + + console.info( + '\n No seed data, skipping. See scripts/seed.ts to start seeding your database!\n' + ) + } catch (error) { + console.error(error) + } +} diff --git a/__fixtures__/esm-fragment-test-project/scripts/tsconfig.json b/__fixtures__/esm-fragment-test-project/scripts/tsconfig.json new file mode 100644 index 0000000000..5690eec16c --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/scripts/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "noEmit": true, + "allowJs": true, + "esModuleInterop": true, + "target": "ES2023", + "module": "Node16", + "moduleResolution": "Node16", + "paths": { + "$api/*": ["../api/*"], + "api/*": ["../api/*"], + "$api/src/*": ["../api/src/*", "../.redwood/types/mirror/api/src/*"], + "api/src/*": ["../api/src/*", "../.redwood/types/mirror/api/src/*"], + "$web/*": ["../web/*"], + "web/*": ["../web/*"], + "$web/src/*": ["../web/src/*", "../.redwood/types/mirror/web/src/*"], + "web/src/*": ["../web/src/*", "../.redwood/types/mirror/web/src/*"], + "types/*": ["../types/*", "../web/types/*", "../api/types/*"] + }, + "typeRoots": ["../node_modules/@types"], + "jsx": "preserve" + }, + "include": [ + ".", + "../.redwood/types/includes/all-*", + "../.redwood/types/includes/web-*", + "../types" + ] +} diff --git a/__fixtures__/esm-fragment-test-project/vitest.config.ts b/__fixtures__/esm-fragment-test-project/vitest.config.ts new file mode 100644 index 0000000000..6c64e7920b --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + projects: ['/{*,!(node_modules)/**/}/vite?(st).config.{js,ts}'], + }, +}) diff --git a/__fixtures__/esm-fragment-test-project/web/config/postcss.config.cjs b/__fixtures__/esm-fragment-test-project/web/config/postcss.config.cjs new file mode 100644 index 0000000000..f6d96e144e --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/config/postcss.config.cjs @@ -0,0 +1,9 @@ +const path = require('path') + +module.exports = { + plugins: [ + require('tailwindcss/nesting'), + require('tailwindcss')(path.resolve(__dirname, 'tailwind.config.cjs')), + require('autoprefixer'), + ], +} diff --git a/__fixtures__/esm-fragment-test-project/web/config/tailwind.config.cjs b/__fixtures__/esm-fragment-test-project/web/config/tailwind.config.cjs new file mode 100644 index 0000000000..613a94fcab --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/config/tailwind.config.cjs @@ -0,0 +1,10 @@ +const { join } = require('node:path') + +/** @type {import('tailwindcss').Config} */ +module.exports = { + content: [join(__dirname, '../src/**/*.{js,jsx,ts,tsx}')], + theme: { + extend: {}, + }, + plugins: [], +} diff --git a/__fixtures__/esm-fragment-test-project/web/package.json b/__fixtures__/esm-fragment-test-project/web/package.json new file mode 100644 index 0000000000..63050afa37 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/package.json @@ -0,0 +1,32 @@ +{ + "name": "web", + "type": "module", + "version": "0.0.0", + "private": true, + "browserslist": { + "development": [ + "last 1 version" + ], + "production": [ + "defaults" + ] + }, + "dependencies": { + "@cedarjs/auth-dbauth-web": "0.0.5", + "@cedarjs/forms": "0.0.5", + "@cedarjs/router": "0.0.5", + "@cedarjs/web": "0.0.5", + "humanize-string": "2.1.0", + "react": "19.0.0-rc-f2df5694-20240916", + "react-dom": "19.0.0-rc-f2df5694-20240916" + }, + "devDependencies": { + "@cedarjs/vite": "0.0.5", + "@types/react": "^18.2.55", + "@types/react-dom": "^18.2.19", + "autoprefixer": "^10.4.21", + "postcss": "^8.5.6", + "postcss-loader": "^8.1.1", + "tailwindcss": "^3.4.17" + } +} diff --git a/__fixtures__/esm-fragment-test-project/web/public/README.md b/__fixtures__/esm-fragment-test-project/web/public/README.md new file mode 100644 index 0000000000..1b09bf8361 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/public/README.md @@ -0,0 +1,43 @@ +# Static Assets + +Use this folder to add static files directly to your app. All included files and folders will be copied directly into the `/dist` folder (created when Vite builds for production). They will also be available during development when you run `yarn rw dev`. + +> Note: files will _not_ hot reload while the development server is running. You'll need to manually stop/start to access file changes. + +### Example Use + +A file like `favicon.png` will be copied to `/dist/favicon.png`. A folder containing a file such as `static-files/my-logo.jpg` will be copied to `/dist/static-files/my-logo.jpg`. These can be referenced in your code directly without any special handling, e.g. + +``` + +``` + +and + +``` + alt="Logo" /> +``` + +## Best Practices + +Because assets in this folder are bypassing the javascript module system, **this folder should be used sparingly** for assets such as favicons, robots.txt, manifests, libraries incompatible with Vite, etc. + +In general, it's best to import files directly into a template, page, or component. This allows Vite to include that file in the bundle when small enough, or to copy it over to the `dist` folder with a hash. + +### Example Asset Import with Vite + +Instead of handling our logo image as a static file per the example above, we can do the following: + +``` +import React from "react" +import logo from "./my-logo.jpg" + + +function Header() { + return Logo +} + +export default Header +``` + +See Vite's docs for [static asset handling](https://vitejs.dev/guide/assets.html) diff --git a/__fixtures__/esm-fragment-test-project/web/public/favicon.png b/__fixtures__/esm-fragment-test-project/web/public/favicon.png new file mode 100644 index 0000000000..1232ba73ab Binary files /dev/null and b/__fixtures__/esm-fragment-test-project/web/public/favicon.png differ diff --git a/__fixtures__/esm-fragment-test-project/web/public/robots.txt b/__fixtures__/esm-fragment-test-project/web/public/robots.txt new file mode 100644 index 0000000000..eb0536286f --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/__fixtures__/esm-fragment-test-project/web/src/App.tsx b/__fixtures__/esm-fragment-test-project/web/src/App.tsx new file mode 100644 index 0000000000..535dc0ae47 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/App.tsx @@ -0,0 +1,29 @@ +import type { ReactNode } from 'react' + +import { FatalErrorBoundary, RedwoodProvider } from '@cedarjs/web' +import { RedwoodApolloProvider } from '@cedarjs/web/apollo' + +import FatalErrorPage from 'src/pages/FatalErrorPage' + +import { AuthProvider, useAuth } from './auth.js' + +import './index.css' +import './scaffold.css' + +interface AppProps { + children?: ReactNode +} + +const App = ({ children }: AppProps) => ( + + + + + {children} + + + + +) + +export default App diff --git a/__fixtures__/esm-fragment-test-project/web/src/Redwood.stories.mdx b/__fixtures__/esm-fragment-test-project/web/src/Redwood.stories.mdx new file mode 100644 index 0000000000..d0ef9e47cf --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/Redwood.stories.mdx @@ -0,0 +1,18 @@ +import { Meta } from '@storybook/addon-docs' + + + +

+ +

Redwood

+

+ +_by Tom Preston-Werner, Peter Pistorius, Rob Cameron, David Price, and more than +250 amazing contributors (see end of file for a full list)._ + +**Redwood is an opinionated, full-stack, JavaScript/TypeScript web application +framework designed to keep you moving fast as your app grows from side project +to startup.** diff --git a/__fixtures__/esm-fragment-test-project/web/src/Routes.tsx b/__fixtures__/esm-fragment-test-project/web/src/Routes.tsx new file mode 100644 index 0000000000..157454cad9 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/Routes.tsx @@ -0,0 +1,54 @@ +// In this file, all Page components from 'src/pages` are auto-imported. Nested +// directories are supported, and should be uppercase. Each subdirectory will be +// prepended onto the component name. +// +// Examples: +// +// 'src/pages/HomePage/HomePage.js' -> HomePage +// 'src/pages/Admin/BooksPage/BooksPage.js' -> AdminBooksPage + +import { Router, Route, Private, Set } from '@cedarjs/router' + +import BlogLayout from 'src/layouts/BlogLayout' +import ScaffoldLayout from 'src/layouts/ScaffoldLayout' +import HomePage from 'src/pages/HomePage' + +import { useAuth } from './auth.js' + +const Routes = () => { + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +export default Routes diff --git a/__fixtures__/esm-fragment-test-project/web/src/auth.ts b/__fixtures__/esm-fragment-test-project/web/src/auth.ts new file mode 100644 index 0000000000..ca02125c24 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/auth.ts @@ -0,0 +1,5 @@ +import { createDbAuthClient, createAuth } from '@cedarjs/auth-dbauth-web' + +const dbAuthClient = createDbAuthClient() + +export const { AuthProvider, useAuth } = createAuth(dbAuthClient) diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/.keep b/__fixtures__/esm-fragment-test-project/web/src/components/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Author/Author.stories.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Author/Author.stories.tsx new file mode 100644 index 0000000000..29dbcf59fa --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/Author/Author.stories.tsx @@ -0,0 +1,35 @@ +// Pass props to your component by passing an `args` object to your story +// +// ```tsx +// export const Primary: Story = { +// args: { +// propName: propValue +// } +// } +// ``` +// +// See https://storybook.js.org/docs/7/writing-stories/args + +import type { Meta, StoryObj } from '@storybook/react' + +import Author from './Author' + +const meta: Meta = { + component: Author, + tags: ['autodocs'], +} + +export default meta + +type Story = StoryObj + +const author = { + email: 'story.user@email.com', + fullName: 'Story User', +} + +export const Primary: Story = { + render: () => { + return + }, +} diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Author/Author.test.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Author/Author.test.tsx new file mode 100644 index 0000000000..09e58d145e --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/Author/Author.test.tsx @@ -0,0 +1,19 @@ +import { render } from '@cedarjs/testing/web' + +import Author from './Author' + +const author = { + email: 'test.user@email.com', + fullName: 'Test User', +} + +// Improve this test with help from the Redwood Testing Doc: +// https://redwoodjs.com/docs/testing#testing-components + +describe('Author', () => { + it('renders successfully', () => { + expect(() => { + render() + }).not.toThrow() + }) +}) diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Author/Author.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Author/Author.tsx new file mode 100644 index 0000000000..f59f8b98f1 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/Author/Author.tsx @@ -0,0 +1,16 @@ +interface Props { + author: { + email: string + fullName: string + } +} + +const Author = ({ author }: Props) => { + return ( + + {author.fullName} ({author.email}) + + ) +} + +export default Author diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/AuthorCell/AuthorCell.mock.ts b/__fixtures__/esm-fragment-test-project/web/src/components/AuthorCell/AuthorCell.mock.ts new file mode 100644 index 0000000000..5dd8454b3d --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/AuthorCell/AuthorCell.mock.ts @@ -0,0 +1,9 @@ +// Define your own mock data here: +export const standard = (/* vars, { ctx, req } */) => ({ + author: { + __typename: 'User' as const, + id: 42, + email: 'fortytwo@42.com', + fullName: 'Forty Two', + }, +}) diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/AuthorCell/AuthorCell.stories.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/AuthorCell/AuthorCell.stories.tsx new file mode 100644 index 0000000000..fd0a2b7337 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/AuthorCell/AuthorCell.stories.tsx @@ -0,0 +1,35 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import { Loading, Empty, Failure, Success } from './AuthorCell' +import { standard } from './AuthorCell.mock' + +const meta: Meta = { + title: 'Cells/AuthorCell', + tags: ['autodocs'], +} + +export default meta + +export const loading: StoryObj = { + render: () => { + return Loading ? : <> + }, +} + +export const empty: StoryObj = { + render: () => { + return Empty ? : <> + }, +} + +export const failure: StoryObj = { + render: (args) => { + return Failure ? : <> + }, +} + +export const success: StoryObj = { + render: (args) => { + return Success ? : <> + }, +} diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/AuthorCell/AuthorCell.test.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/AuthorCell/AuthorCell.test.tsx new file mode 100644 index 0000000000..4fbec70cb1 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/AuthorCell/AuthorCell.test.tsx @@ -0,0 +1,42 @@ +import { render } from '@cedarjs/testing/web' + +import { Loading, Empty, Failure, Success } from './AuthorCell' +import { standard } from './AuthorCell.mock' + +// Generated boilerplate tests do not account for all circumstances +// and can fail without adjustments, e.g. Float and DateTime types. +// Please refer to the RedwoodJS Testing Docs: +// https://redwoodjs.com/docs/testing#testing-cells +// https://redwoodjs.com/docs/testing#jest-expect-type-considerations + +describe('AuthorCell', () => { + it('renders Loading successfully', () => { + expect(() => { + render() + }).not.toThrow() + }) + + it('renders Empty successfully', async () => { + expect(() => { + render() + }).not.toThrow() + }) + + it('renders Failure successfully', async () => { + expect(() => { + render() + }).not.toThrow() + }) + + // When you're ready to test the actual output of your component render + // you could test that, for example, certain text is present: + // + // 1. import { screen } from '@cedarjs/testing/web' + // 2. Add test: expect(screen.getByText('Hello, world')).toBeInTheDocument() + + it('renders Success successfully', async () => { + expect(() => { + render() + }).not.toThrow() + }) +}) diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/AuthorCell/AuthorCell.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/AuthorCell/AuthorCell.tsx new file mode 100644 index 0000000000..95180b96f6 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/AuthorCell/AuthorCell.tsx @@ -0,0 +1,42 @@ +import type { + FindAuthorQuery, + FindAuthorQueryVariables, +} from 'types/graphql.js' + +import type { + CellSuccessProps, + CellFailureProps, + TypedDocumentNode, +} from '@cedarjs/web' + +import Author from 'src/components/Author' + +export const QUERY: TypedDocumentNode< + FindAuthorQuery, + FindAuthorQueryVariables +> = gql` + query FindAuthorQuery($id: Int!) { + author: user(id: $id) { + email + fullName + } + } +` + +export const Loading = () => Loading... + +export const Empty = () => Empty + +export const Failure = ({ + error, +}: CellFailureProps) => ( + Error: {error?.message} +) + +export const Success = ({ + author, +}: CellSuccessProps) => ( + + + +) diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/BlogPost/BlogPost.stories.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPost/BlogPost.stories.tsx new file mode 100644 index 0000000000..4c4daef53f --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPost/BlogPost.stories.tsx @@ -0,0 +1,26 @@ +// Pass props to your component by passing an `args` object to your story +// +// ```tsx +// export const Primary: Story = { +// args: { +// propName: propValue +// } +// } +// ``` +// +// See https://storybook.js.org/docs/7/writing-stories/args + +import type { Meta, StoryObj } from '@storybook/react' + +import BlogPost from './BlogPost' + +const meta: Meta = { + component: BlogPost, + tags: ['autodocs'], +} + +export default meta + +type Story = StoryObj + +export const Primary: Story = {} diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/BlogPost/BlogPost.test.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPost/BlogPost.test.tsx new file mode 100644 index 0000000000..c30451e8ad --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPost/BlogPost.test.tsx @@ -0,0 +1,14 @@ +import { render } from '@cedarjs/testing/web' + +import BlogPost from './BlogPost' + +// Improve this test with help from the Redwood Testing Doc: +// https://redwoodjs.com/docs/testing#testing-components + +describe('BlogPost', () => { + it('renders successfully', () => { + expect(() => { + render() + }).not.toThrow() + }) +}) diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/BlogPost/BlogPost.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPost/BlogPost.tsx new file mode 100644 index 0000000000..e13a2032e7 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPost/BlogPost.tsx @@ -0,0 +1,41 @@ +import { FindBlogPostQuery } from 'types/graphql.js' + +import { Link, routes } from '@cedarjs/router' + +import Author from 'src/components/Author' + +interface Props extends FindBlogPostQuery {} + +const BlogPost = ({ blogPost }: Props) => { + return ( +
+ {blogPost && ( + <> +
+

+ {new Intl.DateTimeFormat('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + }).format(new Date(blogPost.createdAt))}{' '} + - By: +

+

+ + {blogPost.title} + +

+
+
+ {blogPost.body} +
+ + )} +
+ ) +} + +export default BlogPost diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostCell/BlogPostCell.mock.ts b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostCell/BlogPostCell.mock.ts new file mode 100644 index 0000000000..e805ec44db --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostCell/BlogPostCell.mock.ts @@ -0,0 +1,17 @@ +// Define your own mock data here: +export const standard = (/* vars, { ctx, req } */) => ({ + blogPost: { + __typename: 'Post' as const, + id: 42, + title: 'Mocked title', + body: 'Mocked body', + createdAt: '2022-01-17T13:57:51.607Z', + authorId: 5, + + author: { + __typename: 'User' as const, + email: 'five@5.com', + fullName: 'Five Lastname', + }, + }, +}) diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostCell/BlogPostCell.stories.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostCell/BlogPostCell.stories.tsx new file mode 100644 index 0000000000..361d13a911 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostCell/BlogPostCell.stories.tsx @@ -0,0 +1,35 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import { Loading, Empty, Failure, Success } from './BlogPostCell' +import { standard } from './BlogPostCell.mock' + +const meta: Meta = { + title: 'Cells/BlogPostCell', + tags: ['autodocs'], +} + +export default meta + +export const loading: StoryObj = { + render: () => { + return Loading ? : <> + }, +} + +export const empty: StoryObj = { + render: () => { + return Empty ? : <> + }, +} + +export const failure: StoryObj = { + render: (args) => { + return Failure ? : <> + }, +} + +export const success: StoryObj = { + render: (args) => { + return Success ? : <> + }, +} diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostCell/BlogPostCell.test.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostCell/BlogPostCell.test.tsx new file mode 100644 index 0000000000..9df5744f80 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostCell/BlogPostCell.test.tsx @@ -0,0 +1,42 @@ +import { render } from '@cedarjs/testing/web' + +import { Loading, Empty, Failure, Success } from './BlogPostCell' +import { standard } from './BlogPostCell.mock' + +// Generated boilerplate tests do not account for all circumstances +// and can fail without adjustments, e.g. Float and DateTime types. +// Please refer to the RedwoodJS Testing Docs: +// https://redwoodjs.com/docs/testing#testing-cells +// https://redwoodjs.com/docs/testing#jest-expect-type-considerations + +describe('BlogPostCell', () => { + it('renders Loading successfully', () => { + expect(() => { + render() + }).not.toThrow() + }) + + it('renders Empty successfully', async () => { + expect(() => { + render() + }).not.toThrow() + }) + + it('renders Failure successfully', async () => { + expect(() => { + render() + }).not.toThrow() + }) + + // When you're ready to test the actual output of your component render + // you could test that, for example, certain text is present: + // + // 1. import { screen } from '@cedarjs/testing/web' + // 2. Add test: expect(screen.getByText('Hello, world')).toBeInTheDocument() + + it('renders Success successfully', async () => { + expect(() => { + render() + }).not.toThrow() + }) +}) diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostCell/BlogPostCell.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostCell/BlogPostCell.tsx new file mode 100644 index 0000000000..d02ba269df --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostCell/BlogPostCell.tsx @@ -0,0 +1,46 @@ +import type { + FindBlogPostQuery, + FindBlogPostQueryVariables, +} from 'types/graphql.js' + +import type { + CellSuccessProps, + CellFailureProps, + TypedDocumentNode, +} from '@cedarjs/web' + +import BlogPost from 'src/components/BlogPost' + +export const QUERY: TypedDocumentNode< + FindBlogPostQuery, + FindBlogPostQueryVariables +> = gql` + query FindBlogPostQuery($id: Int!) { + blogPost: post(id: $id) { + id + title + body + author { + email + fullName + } + createdAt + } + } +` + +export const Loading = () =>
Loading...
+ +export const Empty = () =>
Empty
+ +export const Failure = ({ + error, +}: CellFailureProps) => ( +
Error: {error?.message}
+) + +export const Success = ({ + blogPost, +}: CellSuccessProps) => ( + +) diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostsCell/BlogPostsCell.mock.ts b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostsCell/BlogPostsCell.mock.ts new file mode 100644 index 0000000000..b1569845f5 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostsCell/BlogPostsCell.mock.ts @@ -0,0 +1,47 @@ +// Define your own mock data here: +export const standard = (/* vars, { ctx, req } */) => ({ + blogPosts: [ + { + __typename: 'Post' as const, + id: 42, + title: 'Mocked title', + body: 'Mocked body', + createdAt: '2022-01-17T13:57:51.607Z', + authorId: 5, + + author: { + __typename: 'User' as const, + email: 'five@5.com', + fullName: 'Five Lastname', + }, + }, + { + __typename: 'Post' as const, + id: 43, + title: 'Mocked title', + body: 'Mocked body', + createdAt: '2022-01-17T13:57:51.607Z', + authorId: 5, + + author: { + __typename: 'User' as const, + email: 'five@5.com', + fullName: 'Five Lastname', + }, + }, + { + __typename: 'Post' as const, + id: 44, + title: 'Mocked title', + body: 'Mocked body', + createdAt: '2022-01-17T13:57:51.607Z', + authorId: 5, + + author: { + __typename: 'User' as const, + email: 'five@5.com', + fullName: 'Five Lastname', + }, + }, + ], +}) diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostsCell/BlogPostsCell.stories.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostsCell/BlogPostsCell.stories.tsx new file mode 100644 index 0000000000..bbd0cb8b0f --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostsCell/BlogPostsCell.stories.tsx @@ -0,0 +1,35 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import { Loading, Empty, Failure, Success } from './BlogPostsCell' +import { standard } from './BlogPostsCell.mock' + +const meta: Meta = { + title: 'Cells/BlogPostsCell', + tags: ['autodocs'], +} + +export default meta + +export const loading: StoryObj = { + render: () => { + return Loading ? : <> + }, +} + +export const empty: StoryObj = { + render: () => { + return Empty ? : <> + }, +} + +export const failure: StoryObj = { + render: (args) => { + return Failure ? : <> + }, +} + +export const success: StoryObj = { + render: (args) => { + return Success ? : <> + }, +} diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostsCell/BlogPostsCell.test.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostsCell/BlogPostsCell.test.tsx new file mode 100644 index 0000000000..5e7a947c9d --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostsCell/BlogPostsCell.test.tsx @@ -0,0 +1,42 @@ +import { render } from '@cedarjs/testing/web' + +import { Loading, Empty, Failure, Success } from './BlogPostsCell' +import { standard } from './BlogPostsCell.mock' + +// Generated boilerplate tests do not account for all circumstances +// and can fail without adjustments, e.g. Float and DateTime types. +// Please refer to the RedwoodJS Testing Docs: +// https://redwoodjs.com/docs/testing#testing-cells +// https://redwoodjs.com/docs/testing#jest-expect-type-considerations + +describe('BlogPostsCell', () => { + it('renders Loading successfully', () => { + expect(() => { + render() + }).not.toThrow() + }) + + it('renders Empty successfully', async () => { + expect(() => { + render() + }).not.toThrow() + }) + + it('renders Failure successfully', async () => { + expect(() => { + render() + }).not.toThrow() + }) + + // When you're ready to test the actual output of your component render + // you could test that, for example, certain text is present: + // + // 1. import { screen } from '@cedarjs/testing/web' + // 2. Add test: expect(screen.getByText('Hello, world')).toBeInTheDocument() + + it('renders Success successfully', async () => { + expect(() => { + render() + }).not.toThrow() + }) +}) diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostsCell/BlogPostsCell.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostsCell/BlogPostsCell.tsx new file mode 100644 index 0000000000..3f77a396ed --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/BlogPostsCell/BlogPostsCell.tsx @@ -0,0 +1,45 @@ +import type { BlogPostsQuery, BlogPostsQueryVariables } from 'types/graphql.js' + +import type { + CellSuccessProps, + CellFailureProps, + TypedDocumentNode, +} from '@cedarjs/web' + +import BlogPost from 'src/components/BlogPost' + +export const QUERY: TypedDocumentNode = + gql` + query BlogPostsQuery { + blogPosts: posts { + id + title + body + author { + email + fullName + } + createdAt + } + } + ` + +export const Loading = () =>
Loading...
+ +export const Empty = () =>
Empty
+ +export const Failure = ({ + error, +}: CellFailureProps) => ( +
Error: {error?.message}
+) + +export const Success = ({ + blogPosts, +}: CellSuccessProps) => ( +
+ {blogPosts.map((post) => ( + + ))} +
+) diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Card.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Card.tsx new file mode 100644 index 0000000000..8894a447b2 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/Card.tsx @@ -0,0 +1,9 @@ +const Card = ({ children }) => { + return ( +
+ {children} +
+ ) +} + +export default Card diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/ClassWithClassField/ClassWithClassField.stories.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/ClassWithClassField/ClassWithClassField.stories.tsx new file mode 100644 index 0000000000..b6855a5b07 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/ClassWithClassField/ClassWithClassField.stories.tsx @@ -0,0 +1,26 @@ +// Pass props to your component by passing an `args` object to your story +// +// ```tsx +// export const Primary: Story = { +// args: { +// propName: propValue +// } +// } +// ``` +// +// See https://storybook.js.org/docs/7/writing-stories/args + +import type { Meta, StoryObj } from '@storybook/react' + +import ClassWithClassField from './ClassWithClassField' + +const meta: Meta = { + component: ClassWithClassField, + tags: ['autodocs'], +} + +export default meta + +type Story = StoryObj + +export const Primary: Story = {} diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/ClassWithClassField/ClassWithClassField.test.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/ClassWithClassField/ClassWithClassField.test.tsx new file mode 100644 index 0000000000..0f342ba74b --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/ClassWithClassField/ClassWithClassField.test.tsx @@ -0,0 +1,14 @@ +import { render } from '@cedarjs/testing/web' + +import ClassWithClassField from './ClassWithClassField' + +// Improve this test with help from the Redwood Testing Doc: +// https://redwoodjs.com/docs/testing#testing-components + +describe('ClassWithClassField', () => { + it('renders successfully', () => { + expect(() => { + render() + }).not.toThrow() + }) +}) diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/ClassWithClassField/ClassWithClassField.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/ClassWithClassField/ClassWithClassField.tsx new file mode 100644 index 0000000000..10cf41bad5 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/ClassWithClassField/ClassWithClassField.tsx @@ -0,0 +1,12 @@ +class Bar {} + +class Foo { + // Without the correct babel plugins this will throw an error + public bar = new Bar() +} + +const ClassWithClassField = () => { + return

{new Foo().bar.toString()}

+} + +export default ClassWithClassField diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Contact/Contact/Contact.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Contact/Contact/Contact.tsx new file mode 100644 index 0000000000..f90bff1d17 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/Contact/Contact/Contact.tsx @@ -0,0 +1,98 @@ +import type { + DeleteContactMutation, + DeleteContactMutationVariables, + FindContactById, +} from 'types/graphql.js' + +import { Link, routes, navigate } from '@cedarjs/router' +import { useMutation } from '@cedarjs/web' +import type { TypedDocumentNode } from '@cedarjs/web' +import { toast } from '@cedarjs/web/toast' + +import { timeTag } from 'src/lib/formatters.js' + +const DELETE_CONTACT_MUTATION: TypedDocumentNode< + DeleteContactMutation, + DeleteContactMutationVariables +> = gql` + mutation DeleteContactMutation($id: Int!) { + deleteContact(id: $id) { + id + } + } +` + +interface Props { + contact: NonNullable +} + +const Contact = ({ contact }: Props) => { + const [deleteContact] = useMutation(DELETE_CONTACT_MUTATION, { + onCompleted: () => { + toast.success('Contact deleted') + navigate(routes.contacts()) + }, + onError: (error) => { + toast.error(error.message) + }, + }) + + const onDeleteClick = (id: DeleteContactMutationVariables['id']) => { + if (confirm('Are you sure you want to delete contact ' + id + '?')) { + deleteContact({ variables: { id } }) + } + } + + return ( + <> +
+
+

+ Contact {contact.id} Detail +

+
+ + + + + + + + + + + + + + + + + + + + + + + +
Id{contact.id}
Name{contact.name}
Email{contact.email}
Message{contact.message}
Created at{timeTag(contact.createdAt)}
+
+ + + ) +} + +export default Contact diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Contact/ContactCell/ContactCell.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Contact/ContactCell/ContactCell.tsx new file mode 100644 index 0000000000..c3470c7360 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/Contact/ContactCell/ContactCell.tsx @@ -0,0 +1,43 @@ +import type { + FindContactById, + FindContactByIdVariables, +} from 'types/graphql.js' + +import type { + CellSuccessProps, + CellFailureProps, + TypedDocumentNode, +} from '@cedarjs/web' + +import Contact from 'src/components/Contact/Contact' + +export const QUERY: TypedDocumentNode< + FindContactById, + FindContactByIdVariables +> = gql` + query FindContactById($id: Int!) { + contact: contact(id: $id) { + id + name + email + message + createdAt + } + } +` + +export const Loading = () =>
Loading...
+ +export const Empty = () =>
Contact not found
+ +export const Failure = ({ + error, +}: CellFailureProps) => ( +
{error?.message}
+) + +export const Success = ({ + contact, +}: CellSuccessProps) => { + return +} diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Contact/ContactForm/ContactForm.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Contact/ContactForm/ContactForm.tsx new file mode 100644 index 0000000000..024b762b2d --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/Contact/ContactForm/ContactForm.tsx @@ -0,0 +1,101 @@ +import type { EditContactById, UpdateContactInput } from 'types/graphql.js' + +import type { RWGqlError } from '@cedarjs/forms' +import { + Form, + FormError, + FieldError, + Label, + TextField, + Submit, +} from '@cedarjs/forms' + +type FormContact = NonNullable + +interface ContactFormProps { + contact?: EditContactById['contact'] + onSave: (data: UpdateContactInput, id?: FormContact['id']) => void + error: RWGqlError + loading: boolean +} + +const ContactForm = (props: ContactFormProps) => { + const onSubmit = (data: FormContact) => { + props.onSave(data, props?.contact?.id) + } + + return ( +
+ onSubmit={onSubmit} error={props.error}> + + + + + + + + + + + + + + + + + + + + +
+ + Save + +
+ +
+ ) +} + +export default ContactForm diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Contact/Contacts/Contacts.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Contact/Contacts/Contacts.tsx new file mode 100644 index 0000000000..30528f3f3f --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/Contact/Contacts/Contacts.tsx @@ -0,0 +1,102 @@ +import type { + DeleteContactMutation, + DeleteContactMutationVariables, + FindContacts, +} from 'types/graphql.js' + +import { Link, routes } from '@cedarjs/router' +import { useMutation } from '@cedarjs/web' +import type { TypedDocumentNode } from '@cedarjs/web' +import { toast } from '@cedarjs/web/toast' + +import { QUERY } from 'src/components/Contact/ContactsCell' +import { timeTag, truncate } from 'src/lib/formatters.js' + +const DELETE_CONTACT_MUTATION: TypedDocumentNode< + DeleteContactMutation, + DeleteContactMutationVariables +> = gql` + mutation DeleteContactMutation($id: Int!) { + deleteContact(id: $id) { + id + } + } +` + +const ContactsList = ({ contacts }: FindContacts) => { + const [deleteContact] = useMutation(DELETE_CONTACT_MUTATION, { + onCompleted: () => { + toast.success('Contact deleted') + }, + onError: (error) => { + toast.error(error.message) + }, + // This refetches the query on the list page. Read more about other ways to + // update the cache over here: + // https://www.apollographql.com/docs/react/data/mutations/#making-all-other-cache-updates + refetchQueries: [{ query: QUERY }], + awaitRefetchQueries: true, + }) + + const onDeleteClick = (id: DeleteContactMutationVariables['id']) => { + if (confirm('Are you sure you want to delete contact ' + id + '?')) { + deleteContact({ variables: { id } }) + } + } + + return ( +
+ + + + + + + + + + + + + {contacts.map((contact) => ( + + + + + + + + + ))} + +
IdNameEmailMessageCreated at 
{truncate(contact.id)}{truncate(contact.name)}{truncate(contact.email)}{truncate(contact.message)}{timeTag(contact.createdAt)} + +
+
+ ) +} + +export default ContactsList diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Contact/ContactsCell/ContactsCell.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Contact/ContactsCell/ContactsCell.tsx new file mode 100644 index 0000000000..e64d6a8e96 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/Contact/ContactsCell/ContactsCell.tsx @@ -0,0 +1,46 @@ +import type { FindContacts, FindContactsVariables } from 'types/graphql.js' + +import { Link, routes } from '@cedarjs/router' +import type { + CellSuccessProps, + CellFailureProps, + TypedDocumentNode, +} from '@cedarjs/web' + +import Contacts from 'src/components/Contact/Contacts' + +export const QUERY: TypedDocumentNode = + gql` + query FindContacts { + contacts { + id + name + email + message + createdAt + } + } + ` + +export const Loading = () =>
Loading...
+ +export const Empty = () => { + return ( +
+ No contacts yet.{' '} + + Create one? + +
+ ) +} + +export const Failure = ({ error }: CellFailureProps) => ( +
{error?.message}
+) + +export const Success = ({ + contacts, +}: CellSuccessProps) => { + return +} diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Contact/EditContactCell/EditContactCell.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Contact/EditContactCell/EditContactCell.tsx new file mode 100644 index 0000000000..622e63f686 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/Contact/EditContactCell/EditContactCell.tsx @@ -0,0 +1,89 @@ +import type { + EditContactById, + UpdateContactInput, + UpdateContactMutationVariables, +} from 'types/graphql.js' + +import { navigate, routes } from '@cedarjs/router' +import type { + CellSuccessProps, + CellFailureProps, + TypedDocumentNode, +} from '@cedarjs/web' +import { useMutation } from '@cedarjs/web' +import { toast } from '@cedarjs/web/toast' + +import ContactForm from 'src/components/Contact/ContactForm' + +export const QUERY: TypedDocumentNode = gql` + query EditContactById($id: Int!) { + contact: contact(id: $id) { + id + name + email + message + createdAt + } + } +` + +const UPDATE_CONTACT_MUTATION: TypedDocumentNode< + EditContactById, + UpdateContactMutationVariables +> = gql` + mutation UpdateContactMutation($id: Int!, $input: UpdateContactInput!) { + updateContact(id: $id, input: $input) { + id + name + email + message + createdAt + } + } +` + +export const Loading = () =>
Loading...
+ +export const Failure = ({ error }: CellFailureProps) => ( +
{error?.message}
+) + +export const Success = ({ contact }: CellSuccessProps) => { + const [updateContact, { loading, error }] = useMutation( + UPDATE_CONTACT_MUTATION, + { + onCompleted: () => { + toast.success('Contact updated') + navigate(routes.contacts()) + }, + onError: (error) => { + toast.error(error.message) + }, + } + ) + + const onSave = ( + input: UpdateContactInput, + id: EditContactById['contact']['id'] + ) => { + updateContact({ variables: { id, input } }) + } + + return ( +
+
+

+ Edit Contact {contact?.id} +

+
+
+ +
+
+ ) +} diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Contact/NewContact/NewContact.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Contact/NewContact/NewContact.tsx new file mode 100644 index 0000000000..2cb66a3c30 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/Contact/NewContact/NewContact.tsx @@ -0,0 +1,55 @@ +import type { + CreateContactMutation, + CreateContactInput, + CreateContactMutationVariables, +} from 'types/graphql.js' + +import { navigate, routes } from '@cedarjs/router' +import { useMutation } from '@cedarjs/web' +import type { TypedDocumentNode } from '@cedarjs/web' +import { toast } from '@cedarjs/web/toast' + +import ContactForm from 'src/components/Contact/ContactForm' + +const CREATE_CONTACT_MUTATION: TypedDocumentNode< + CreateContactMutation, + CreateContactMutationVariables +> = gql` + mutation CreateContactMutation($input: CreateContactInput!) { + createContact(input: $input) { + id + } + } +` + +const NewContact = () => { + const [createContact, { loading, error }] = useMutation( + CREATE_CONTACT_MUTATION, + { + onCompleted: () => { + toast.success('Contact created') + navigate(routes.contacts()) + }, + onError: (error) => { + toast.error(error.message) + }, + } + ) + + const onSave = (input: CreateContactInput) => { + createContact({ variables: { input } }) + } + + return ( +
+
+

New Contact

+
+
+ +
+
+ ) +} + +export default NewContact diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/FruitInfo.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/FruitInfo.tsx new file mode 100644 index 0000000000..54fb6f98b0 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/FruitInfo.tsx @@ -0,0 +1,35 @@ +import type { Fruit } from 'types/graphql.js' + +import { registerFragment } from '@cedarjs/web/apollo' + +import Card from 'src/components/Card' +import StallInfo from 'src/components/StallInfo' + +const { useRegisteredFragment } = registerFragment(gql` + fragment Fruit_info on Fruit { + id + name + isSeedless + ripenessIndicators + stall { + ...Stall_info + } + } +`) + +const FruitInfo = ({ id }: { id: string }) => { + const { data: fruit, complete } = useRegisteredFragment(id) + + return ( + complete && ( + +

Fruit Name: {fruit.name}

+

Seeds? {fruit.isSeedless ? 'Yes' : 'No'}

+

Ripeness: {fruit.ripenessIndicators}

+ +
+ ) + ) +} + +export default FruitInfo diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Post/EditPostCell/EditPostCell.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Post/EditPostCell/EditPostCell.tsx new file mode 100644 index 0000000000..53a0bd1079 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/Post/EditPostCell/EditPostCell.tsx @@ -0,0 +1,78 @@ +import type { + EditPostById, + UpdatePostInput, + UpdatePostMutationVariables, +} from 'types/graphql.js' + +import { navigate, routes } from '@cedarjs/router' +import type { + CellSuccessProps, + CellFailureProps, + TypedDocumentNode, +} from '@cedarjs/web' +import { useMutation } from '@cedarjs/web' +import { toast } from '@cedarjs/web/toast' + +import PostForm from 'src/components/Post/PostForm' + +export const QUERY: TypedDocumentNode = gql` + query EditPostById($id: Int!) { + post: post(id: $id) { + id + title + body + authorId + createdAt + } + } +` + +const UPDATE_POST_MUTATION: TypedDocumentNode< + EditPostById, + UpdatePostMutationVariables +> = gql` + mutation UpdatePostMutation($id: Int!, $input: UpdatePostInput!) { + updatePost(id: $id, input: $input) { + id + title + body + authorId + createdAt + } + } +` + +export const Loading = () =>
Loading...
+ +export const Failure = ({ error }: CellFailureProps) => ( +
{error?.message}
+) + +export const Success = ({ post }: CellSuccessProps) => { + const [updatePost, { loading, error }] = useMutation(UPDATE_POST_MUTATION, { + onCompleted: () => { + toast.success('Post updated') + navigate(routes.posts()) + }, + onError: (error) => { + toast.error(error.message) + }, + }) + + const onSave = (input: UpdatePostInput, id: EditPostById['post']['id']) => { + updatePost({ variables: { id, input } }) + } + + return ( +
+
+

+ Edit Post {post?.id} +

+
+
+ +
+
+ ) +} diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Post/NewPost/NewPost.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Post/NewPost/NewPost.tsx new file mode 100644 index 0000000000..4a3a8b91bd --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/Post/NewPost/NewPost.tsx @@ -0,0 +1,52 @@ +import type { + CreatePostMutation, + CreatePostInput, + CreatePostMutationVariables, +} from 'types/graphql.js' + +import { navigate, routes } from '@cedarjs/router' +import { useMutation } from '@cedarjs/web' +import type { TypedDocumentNode } from '@cedarjs/web' +import { toast } from '@cedarjs/web/toast' + +import PostForm from 'src/components/Post/PostForm' + +const CREATE_POST_MUTATION: TypedDocumentNode< + CreatePostMutation, + CreatePostMutationVariables +> = gql` + mutation CreatePostMutation($input: CreatePostInput!) { + createPost(input: $input) { + id + } + } +` + +const NewPost = () => { + const [createPost, { loading, error }] = useMutation(CREATE_POST_MUTATION, { + onCompleted: () => { + toast.success('Post created') + navigate(routes.posts()) + }, + onError: (error) => { + toast.error(error.message) + }, + }) + + const onSave = (input: CreatePostInput) => { + createPost({ variables: { input } }) + } + + return ( +
+
+

New Post

+
+
+ +
+
+ ) +} + +export default NewPost diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Post/Post/Post.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Post/Post/Post.tsx new file mode 100644 index 0000000000..dfbc5a413c --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/Post/Post/Post.tsx @@ -0,0 +1,98 @@ +import type { + DeletePostMutation, + DeletePostMutationVariables, + FindPostById, +} from 'types/graphql.js' + +import { Link, routes, navigate } from '@cedarjs/router' +import { useMutation } from '@cedarjs/web' +import type { TypedDocumentNode } from '@cedarjs/web' +import { toast } from '@cedarjs/web/toast' + +import { timeTag } from 'src/lib/formatters.js' + +const DELETE_POST_MUTATION: TypedDocumentNode< + DeletePostMutation, + DeletePostMutationVariables +> = gql` + mutation DeletePostMutation($id: Int!) { + deletePost(id: $id) { + id + } + } +` + +interface Props { + post: NonNullable +} + +const Post = ({ post }: Props) => { + const [deletePost] = useMutation(DELETE_POST_MUTATION, { + onCompleted: () => { + toast.success('Post deleted') + navigate(routes.posts()) + }, + onError: (error) => { + toast.error(error.message) + }, + }) + + const onDeleteClick = (id: DeletePostMutationVariables['id']) => { + if (confirm('Are you sure you want to delete post ' + id + '?')) { + deletePost({ variables: { id } }) + } + } + + return ( + <> +
+
+

+ Post {post.id} Detail +

+
+ + + + + + + + + + + + + + + + + + + + + + + +
Id{post.id}
Title{post.title}
Body{post.body}
Author id{post.authorId}
Created at{timeTag(post.createdAt)}
+
+ + + ) +} + +export default Post diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Post/PostCell/PostCell.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Post/PostCell/PostCell.tsx new file mode 100644 index 0000000000..f156749b0d --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/Post/PostCell/PostCell.tsx @@ -0,0 +1,36 @@ +import type { FindPostById, FindPostByIdVariables } from 'types/graphql.js' + +import type { + CellSuccessProps, + CellFailureProps, + TypedDocumentNode, +} from '@cedarjs/web' + +import Post from 'src/components/Post/Post' + +export const QUERY: TypedDocumentNode = + gql` + query FindPostById($id: Int!) { + post: post(id: $id) { + id + title + body + authorId + createdAt + } + } + ` + +export const Loading = () =>
Loading...
+ +export const Empty = () =>
Post not found
+ +export const Failure = ({ error }: CellFailureProps) => ( +
{error?.message}
+) + +export const Success = ({ + post, +}: CellSuccessProps) => { + return +} diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Post/PostForm/PostForm.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Post/PostForm/PostForm.tsx new file mode 100644 index 0000000000..4a17df0ca1 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/Post/PostForm/PostForm.tsx @@ -0,0 +1,102 @@ +import type { EditPostById, UpdatePostInput } from 'types/graphql.js' + +import type { RWGqlError } from '@cedarjs/forms' +import { + Form, + FormError, + FieldError, + Label, + TextField, + NumberField, + Submit, +} from '@cedarjs/forms' + +type FormPost = NonNullable + +interface PostFormProps { + post?: EditPostById['post'] + onSave: (data: UpdatePostInput, id?: FormPost['id']) => void + error: RWGqlError + loading: boolean +} + +const PostForm = (props: PostFormProps) => { + const onSubmit = (data: FormPost) => { + props.onSave(data, props?.post?.id) + } + + return ( +
+ onSubmit={onSubmit} error={props.error}> + + + + + + + + + + + + + + + + + + + + +
+ + Save + +
+ +
+ ) +} + +export default PostForm diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Post/Posts/Posts.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Post/Posts/Posts.tsx new file mode 100644 index 0000000000..edbf98fe98 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/Post/Posts/Posts.tsx @@ -0,0 +1,102 @@ +import type { + DeletePostMutation, + DeletePostMutationVariables, + FindPosts, +} from 'types/graphql.js' + +import { Link, routes } from '@cedarjs/router' +import { useMutation } from '@cedarjs/web' +import type { TypedDocumentNode } from '@cedarjs/web' +import { toast } from '@cedarjs/web/toast' + +import { QUERY } from 'src/components/Post/PostsCell' +import { timeTag, truncate } from 'src/lib/formatters.js' + +const DELETE_POST_MUTATION: TypedDocumentNode< + DeletePostMutation, + DeletePostMutationVariables +> = gql` + mutation DeletePostMutation($id: Int!) { + deletePost(id: $id) { + id + } + } +` + +const PostsList = ({ posts }: FindPosts) => { + const [deletePost] = useMutation(DELETE_POST_MUTATION, { + onCompleted: () => { + toast.success('Post deleted') + }, + onError: (error) => { + toast.error(error.message) + }, + // This refetches the query on the list page. Read more about other ways to + // update the cache over here: + // https://www.apollographql.com/docs/react/data/mutations/#making-all-other-cache-updates + refetchQueries: [{ query: QUERY }], + awaitRefetchQueries: true, + }) + + const onDeleteClick = (id: DeletePostMutationVariables['id']) => { + if (confirm('Are you sure you want to delete post ' + id + '?')) { + deletePost({ variables: { id } }) + } + } + + return ( +
+ + + + + + + + + + + + + {posts.map((post) => ( + + + + + + + + + ))} + +
IdTitleBodyAuthor idCreated at 
{truncate(post.id)}{truncate(post.title)}{truncate(post.body)}{truncate(post.authorId)}{timeTag(post.createdAt)} + +
+
+ ) +} + +export default PostsList diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/Post/PostsCell/PostsCell.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/Post/PostsCell/PostsCell.tsx new file mode 100644 index 0000000000..8a3f465701 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/Post/PostsCell/PostsCell.tsx @@ -0,0 +1,45 @@ +import type { FindPosts, FindPostsVariables } from 'types/graphql.js' + +import { Link, routes } from '@cedarjs/router' +import type { + CellSuccessProps, + CellFailureProps, + TypedDocumentNode, +} from '@cedarjs/web' + +import Posts from 'src/components/Post/Posts' + +export const QUERY: TypedDocumentNode = gql` + query FindPosts { + posts { + id + title + body + authorId + createdAt + } + } +` + +export const Loading = () =>
Loading...
+ +export const Empty = () => { + return ( +
+ No posts yet.{' '} + + Create one? + +
+ ) +} + +export const Failure = ({ error }: CellFailureProps) => ( +
{error?.message}
+) + +export const Success = ({ + posts, +}: CellSuccessProps) => { + return +} diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/ProduceInfo.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/ProduceInfo.tsx new file mode 100644 index 0000000000..c70c97cd0f --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/ProduceInfo.tsx @@ -0,0 +1,26 @@ +import type { Produce } from 'types/graphql.js' + +import { registerFragment } from '@cedarjs/web/apollo' + +import Card from 'src/components/Card' + +const { useRegisteredFragment } = registerFragment(gql` + fragment Produce_info on Produce { + id + name + } +`) + +const ProduceInfo = ({ id }: { id: string }) => { + const { data, complete } = useRegisteredFragment(id) + + return ( + complete && ( + +

Produce Name: {data.name}

+
+ ) + ) +} + +export default ProduceInfo diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/StallInfo.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/StallInfo.tsx new file mode 100644 index 0000000000..b828da4b1c --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/StallInfo.tsx @@ -0,0 +1,24 @@ +import type { Stall } from 'types/graphql.js' + +import { registerFragment } from '@cedarjs/web/apollo' + +const { useRegisteredFragment } = registerFragment(gql` + fragment Stall_info on Stall { + id + name + } +`) + +const StallInfo = ({ id }: { id: string }) => { + const { data, complete } = useRegisteredFragment(id) + + return ( + complete && ( +
+

Stall Name: {data.name}

+
+ ) + ) +} + +export default StallInfo diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/VegetableInfo.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/VegetableInfo.tsx new file mode 100644 index 0000000000..71e53a04f6 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/VegetableInfo.tsx @@ -0,0 +1,35 @@ +import type { Vegetable } from 'types/graphql.js' + +import { registerFragment } from '@cedarjs/web/apollo' + +import Card from 'src/components/Card' +import StallInfo from 'src/components/StallInfo' + +const { useRegisteredFragment } = registerFragment(gql` + fragment Vegetable_info on Vegetable { + id + name + vegetableFamily + isPickled + stall { + ...Stall_info + } + } +`) + +const VegetableInfo = ({ id }: { id: string }) => { + const { data: vegetable, complete } = useRegisteredFragment(id) + + return ( + complete && ( + +

Vegetable Name: {vegetable.name}

+

Pickled? {vegetable.isPickled ? 'Yes' : 'No'}

+

Family: {vegetable.vegetableFamily}

+ +
+ ) + ) +} + +export default VegetableInfo diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.mock.ts b/__fixtures__/esm-fragment-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.mock.ts new file mode 100644 index 0000000000..cf02b60610 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.mock.ts @@ -0,0 +1,17 @@ +// Define your own mock data here: +export const standard = (/* vars, { ctx, req } */) => ({ + waterfallBlogPost: { + __typename: 'Post' as const, + id: 42, + title: 'Mocked title', + body: 'Mocked body', + createdAt: '2022-01-17T13:57:51.607Z', + authorId: 7, + + author: { + __typename: 'User' as const, + email: 'se7en@7.com', + fullName: 'Se7en Lastname', + }, + }, +}) diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.stories.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.stories.tsx new file mode 100644 index 0000000000..7109babeb3 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.stories.tsx @@ -0,0 +1,35 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import { Loading, Empty, Failure, Success } from './WaterfallBlogPostCell' +import { standard } from './WaterfallBlogPostCell.mock' + +const meta: Meta = { + title: 'Cells/WaterfallBlogPostCell', + tags: ['autodocs'], +} + +export default meta + +export const loading: StoryObj = { + render: () => { + return Loading ? : <> + }, +} + +export const empty: StoryObj = { + render: () => { + return Empty ? : <> + }, +} + +export const failure: StoryObj = { + render: (args) => { + return Failure ? : <> + }, +} + +export const success: StoryObj = { + render: (args) => { + return Success ? : <> + }, +} diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.test.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.test.tsx new file mode 100644 index 0000000000..6701312f2c --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.test.tsx @@ -0,0 +1,44 @@ +import { render } from '@cedarjs/testing/web' + +import { Loading, Empty, Failure, Success } from './WaterfallBlogPostCell' +import { standard } from './WaterfallBlogPostCell.mock' + +// Generated boilerplate tests do not account for all circumstances +// and can fail without adjustments, e.g. Float and DateTime types. +// Please refer to the RedwoodJS Testing Docs: +// https://redwoodjs.com/docs/testing#testing-cells +// https://redwoodjs.com/docs/testing#jest-expect-type-considerations + +describe('WaterfallBlogPostCell', () => { + it('renders Loading successfully', () => { + expect(() => { + render() + }).not.toThrow() + }) + + it('renders Empty successfully', async () => { + expect(() => { + render() + }).not.toThrow() + }) + + it('renders Failure successfully', async () => { + expect(() => { + render() + }).not.toThrow() + }) + + // When you're ready to test the actual output of your component render + // you could test that, for example, certain text is present: + // + // 1. import { screen } from '@cedarjs/testing/web' + // 2. Add test: expect(screen.getByText('Hello, world')).toBeInTheDocument() + + it('renders Success successfully', async () => { + expect(() => { + render( + + ) + }).not.toThrow() + }) +}) diff --git a/__fixtures__/esm-fragment-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.tsx b/__fixtures__/esm-fragment-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.tsx new file mode 100644 index 0000000000..075ca427a6 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/components/WaterfallBlogPostCell/WaterfallBlogPostCell.tsx @@ -0,0 +1,67 @@ +import type { + FindWaterfallBlogPostQuery, + FindWaterfallBlogPostQueryVariables, +} from 'types/graphql.js' + +import type { + CellSuccessProps, + CellFailureProps, + TypedDocumentNode, +} from '@cedarjs/web' + +import AuthorCell from 'src/components/AuthorCell' + +export const QUERY: TypedDocumentNode< + FindWaterfallBlogPostQuery, + FindWaterfallBlogPostQueryVariables +> = gql` + query FindWaterfallBlogPostQuery($id: Int!) { + waterfallBlogPost: post(id: $id) { + id + title + body + authorId + createdAt + } + } +` + +export const Loading = () =>
Loading...
+ +export const Empty = () =>
Empty
+ +export const Failure = ({ + error, +}: CellFailureProps) => ( +
Error: {error?.message}
+) + +export const Success = ({ + waterfallBlogPost, +}: CellSuccessProps< + FindWaterfallBlogPostQuery, + FindWaterfallBlogPostQueryVariables +>) => ( +
+ {waterfallBlogPost && ( + <> +
+

+ {new Intl.DateTimeFormat('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric', + }).format(new Date(waterfallBlogPost.createdAt))}{' '} + - By: +

+

+ {waterfallBlogPost.title} +

+
+
+ {waterfallBlogPost.body} +
+ + )} +
+) diff --git a/__fixtures__/esm-fragment-test-project/web/src/entry.client.tsx b/__fixtures__/esm-fragment-test-project/web/src/entry.client.tsx new file mode 100644 index 0000000000..915c14d76d --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/entry.client.tsx @@ -0,0 +1,35 @@ +import { hydrateRoot, createRoot } from 'react-dom/client' + +import App from './App' +import Routes from './Routes' + +/** + * When `#redwood-app` isn't empty then it's very likely that you're using + * prerendering. So React attaches event listeners to the existing markup + * rather than replacing it. + * https://react.dev/reference/react-dom/client/hydrateRoot + */ +const redwoodAppElement = document.getElementById('redwood-app') + +if (!redwoodAppElement) { + throw new Error( + "Could not find an element with ID 'redwood-app'. Please ensure it " + + "exists in your 'web/src/index.html' file." + ) +} + +if (redwoodAppElement.children?.length > 0) { + hydrateRoot( + redwoodAppElement, + + + + ) +} else { + const root = createRoot(redwoodAppElement) + root.render( + + + + ) +} diff --git a/__fixtures__/esm-fragment-test-project/web/src/graphql/possibleTypes.ts b/__fixtures__/esm-fragment-test-project/web/src/graphql/possibleTypes.ts new file mode 100644 index 0000000000..889b16ceeb --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/graphql/possibleTypes.ts @@ -0,0 +1,12 @@ +export interface PossibleTypesResultData { + possibleTypes: { + [key: string]: string[] + } +} +const result: PossibleTypesResultData = { + possibleTypes: { + Groceries: ['Fruit', 'Vegetable'], + Grocery: ['Fruit', 'Vegetable'], + }, +} +export default result diff --git a/__fixtures__/esm-fragment-test-project/web/src/index.css b/__fixtures__/esm-fragment-test-project/web/src/index.css new file mode 100644 index 0000000000..b31cb3378f --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/index.css @@ -0,0 +1,13 @@ +/** + * START --- SETUP TAILWINDCSS EDIT + * + * `yarn rw setup ui tailwindcss` placed these directives here + * to inject Tailwind's styles into your CSS. + * For more information, see: https://tailwindcss.com/docs/installation + */ +@tailwind base; +@tailwind components; +@tailwind utilities; +/** + * END --- SETUP TAILWINDCSS EDIT + */ diff --git a/__fixtures__/esm-fragment-test-project/web/src/index.html b/__fixtures__/esm-fragment-test-project/web/src/index.html new file mode 100644 index 0000000000..e240b8eb8c --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/index.html @@ -0,0 +1,15 @@ + + + + + + + + + + + +
+ + + diff --git a/__fixtures__/esm-fragment-test-project/web/src/layouts/.keep b/__fixtures__/esm-fragment-test-project/web/src/layouts/.keep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/__fixtures__/esm-fragment-test-project/web/src/layouts/BlogLayout/BlogLayout.stories.tsx b/__fixtures__/esm-fragment-test-project/web/src/layouts/BlogLayout/BlogLayout.stories.tsx new file mode 100644 index 0000000000..43f3b5d106 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/layouts/BlogLayout/BlogLayout.stories.tsx @@ -0,0 +1,13 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import BlogLayout from './BlogLayout' + +const meta: Meta = { + component: BlogLayout, +} + +export default meta + +type Story = StoryObj + +export const Primary: Story = {} diff --git a/__fixtures__/esm-fragment-test-project/web/src/layouts/BlogLayout/BlogLayout.test.tsx b/__fixtures__/esm-fragment-test-project/web/src/layouts/BlogLayout/BlogLayout.test.tsx new file mode 100644 index 0000000000..60ff1b9368 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/layouts/BlogLayout/BlogLayout.test.tsx @@ -0,0 +1,14 @@ +import { render } from '@cedarjs/testing/web' + +import BlogLayout from './BlogLayout' + +// Improve this test with help from the Redwood Testing Doc: +// https://redwoodjs.com/docs/testing#testing-pages-layouts + +describe('BlogLayout', () => { + it('renders successfully', () => { + expect(() => { + render() + }).not.toThrow() + }) +}) diff --git a/__fixtures__/esm-fragment-test-project/web/src/layouts/BlogLayout/BlogLayout.tsx b/__fixtures__/esm-fragment-test-project/web/src/layouts/BlogLayout/BlogLayout.tsx new file mode 100644 index 0000000000..87af741fa0 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/layouts/BlogLayout/BlogLayout.tsx @@ -0,0 +1,85 @@ +type BlogLayoutProps = { + children?: React.ReactNode +} + +import { Link, NavLink, routes } from '@cedarjs/router' + +import { useAuth } from 'src/auth' + +const BlogLayout = ({ children }: BlogLayoutProps) => { + const { logOut, isAuthenticated } = useAuth() + + return ( + <> +
+

+ + Cedar Blog + +

+ +
+
+ {children} +
+ + ) +} + +export default BlogLayout diff --git a/__fixtures__/esm-fragment-test-project/web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx b/__fixtures__/esm-fragment-test-project/web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx new file mode 100644 index 0000000000..626f0d5cc5 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/layouts/ScaffoldLayout/ScaffoldLayout.tsx @@ -0,0 +1,37 @@ +import { Link, routes } from '@cedarjs/router' +import { Toaster } from '@cedarjs/web/toast' + +type LayoutProps = { + title: string + titleTo: keyof typeof routes + buttonLabel: string + buttonTo: keyof typeof routes + children: React.ReactNode +} + +const ScaffoldLayout = ({ + title, + titleTo, + buttonLabel, + buttonTo, + children, +}: LayoutProps) => { + return ( +
+ +
+

+ + {title} + +

+ +
+
{buttonLabel} + +
+
{children}
+
+ ) +} + +export default ScaffoldLayout diff --git a/__fixtures__/esm-fragment-test-project/web/src/lib/formatters.test.tsx b/__fixtures__/esm-fragment-test-project/web/src/lib/formatters.test.tsx new file mode 100644 index 0000000000..0383c42716 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/lib/formatters.test.tsx @@ -0,0 +1,192 @@ +import { render, waitFor, screen } from '@cedarjs/testing/web' + +import { + formatEnum, + jsonTruncate, + truncate, + timeTag, + jsonDisplay, + checkboxInputTag, +} from './formatters.js' + +describe('formatEnum', () => { + it('handles nullish values', () => { + expect(formatEnum(null)).toEqual('') + expect(formatEnum('')).toEqual('') + expect(formatEnum(undefined)).toEqual('') + }) + + it('formats a list of values', () => { + expect( + formatEnum(['RED', 'ORANGE', 'YELLOW', 'GREEN', 'BLUE', 'VIOLET']) + ).toEqual('Red, Orange, Yellow, Green, Blue, Violet') + }) + + it('formats a single value', () => { + expect(formatEnum('DARK_BLUE')).toEqual('Dark blue') + }) + + it('returns an empty string for values of the wrong type (for JS projects)', () => { + // @ts-expect-error - Testing JS scenario + expect(formatEnum(5)).toEqual('') + }) +}) + +describe('truncate', () => { + it('truncates really long strings', () => { + expect(truncate('na '.repeat(1000) + 'batman').length).toBeLessThan(1000) + expect(truncate('na '.repeat(1000) + 'batman')).not.toMatch(/batman/) + }) + + it('does not modify short strings', () => { + expect(truncate('Short strinG')).toEqual('Short strinG') + }) + + it('adds ... to the end of truncated strings', () => { + expect(truncate('repeat'.repeat(1000))).toMatch(/\w\.\.\.$/) + }) + + it('accepts numbers', () => { + expect(truncate(123)).toEqual('123') + expect(truncate(0)).toEqual('0') + expect(truncate(0o000)).toEqual('0') + }) + + it('handles arguments of invalid type', () => { + // @ts-expect-error - Testing JS scenario + expect(truncate(false)).toEqual('false') + + expect(truncate(undefined)).toEqual('') + expect(truncate(null)).toEqual('') + }) +}) + +describe('jsonTruncate', () => { + it('truncates large json structures', () => { + expect( + jsonTruncate({ + foo: 'foo', + bar: 'bar', + baz: 'baz', + kittens: 'kittens meow', + bazinga: 'Sheldon', + nested: { + foobar: 'I have no imagination', + two: 'Second nested item', + }, + five: 5, + bool: false, + }) + ).toMatch(/.+\n.+\w\.\.\.$/s) + }) +}) + +describe('timeTag', () => { + it('renders a date', async () => { + render(
{timeTag(new Date('1970-08-20').toUTCString())}
) + + await waitFor(() => screen.getByText(/1970.*00:00:00/)) + }) + + it('can take an empty input string', async () => { + expect(timeTag('')).toEqual('') + }) +}) + +describe('jsonDisplay', () => { + it('produces the correct output', () => { + expect( + jsonDisplay({ + title: 'TOML Example (but in JSON)', + database: { + data: [['delta', 'phi'], [3.14]], + enabled: true, + ports: [8000, 8001, 8002], + temp_targets: { + case: 72.0, + cpu: 79.5, + }, + }, + owner: { + dob: '1979-05-27T07:32:00-08:00', + name: 'Tom Preston-Werner', + }, + servers: { + alpha: { + ip: '10.0.0.1', + role: 'frontend', + }, + beta: { + ip: '10.0.0.2', + role: 'backend', + }, + }, + }) + ).toMatchInlineSnapshot(` +
+        
+          {
+        "title": "TOML Example (but in JSON)",
+        "database": {
+          "data": [
+            [
+              "delta",
+              "phi"
+            ],
+            [
+              3.14
+            ]
+          ],
+          "enabled": true,
+          "ports": [
+            8000,
+            8001,
+            8002
+          ],
+          "temp_targets": {
+            "case": 72,
+            "cpu": 79.5
+          }
+        },
+        "owner": {
+          "dob": "1979-05-27T07:32:00-08:00",
+          "name": "Tom Preston-Werner"
+        },
+        "servers": {
+          "alpha": {
+            "ip": "10.0.0.1",
+            "role": "frontend"
+          },
+          "beta": {
+            "ip": "10.0.0.2",
+            "role": "backend"
+          }
+        }
+      }
+        
+      
+ `) + }) +}) + +describe('checkboxInputTag', () => { + it('can be checked', () => { + render(checkboxInputTag(true)) + expect(screen.getByRole('checkbox')).toBeChecked() + }) + + it('can be unchecked', () => { + render(checkboxInputTag(false)) + expect(screen.getByRole('checkbox')).not.toBeChecked() + }) + + it('is disabled when checked', () => { + render(checkboxInputTag(true)) + expect(screen.getByRole('checkbox')).toBeDisabled() + }) + + it('is disabled when unchecked', () => { + render(checkboxInputTag(false)) + expect(screen.getByRole('checkbox')).toBeDisabled() + }) +}) diff --git a/__fixtures__/esm-fragment-test-project/web/src/lib/formatters.tsx b/__fixtures__/esm-fragment-test-project/web/src/lib/formatters.tsx new file mode 100644 index 0000000000..8ab9e806e3 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/lib/formatters.tsx @@ -0,0 +1,58 @@ +import React from 'react' + +import humanize from 'humanize-string' + +const MAX_STRING_LENGTH = 150 + +export const formatEnum = (values: string | string[] | null | undefined) => { + let output = '' + + if (Array.isArray(values)) { + const humanizedValues = values.map((value) => humanize(value)) + output = humanizedValues.join(', ') + } else if (typeof values === 'string') { + output = humanize(values) + } + + return output +} + +export const jsonDisplay = (obj: unknown) => { + return ( +
+      {JSON.stringify(obj, null, 2)}
+    
+ ) +} + +export const truncate = (value: string | number) => { + let output = value?.toString() ?? '' + + if (output.length > MAX_STRING_LENGTH) { + output = output.substring(0, MAX_STRING_LENGTH) + '...' + } + + return output +} + +export const jsonTruncate = (obj: unknown) => { + return truncate(JSON.stringify(obj, null, 2)) +} + +export const timeTag = (dateTime?: string) => { + let output: string | JSX.Element = '' + + if (dateTime) { + output = ( + + ) + } + + return output +} + +export const checkboxInputTag = (checked: boolean) => { + return +} diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/AboutPage/AboutPage.stories.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/AboutPage/AboutPage.stories.tsx new file mode 100644 index 0000000000..b8259100eb --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/pages/AboutPage/AboutPage.stories.tsx @@ -0,0 +1,13 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import AboutPage from './AboutPage' + +const meta: Meta = { + component: AboutPage, +} + +export default meta + +type Story = StoryObj + +export const Primary: Story = {} diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/AboutPage/AboutPage.test.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/AboutPage/AboutPage.test.tsx new file mode 100644 index 0000000000..13f28830eb --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/pages/AboutPage/AboutPage.test.tsx @@ -0,0 +1,14 @@ +import { render } from '@cedarjs/testing/web' + +import AboutPage from './AboutPage' + +// Improve this test with help from the Redwood Testing Doc: +// https://redwoodjs.com/docs/testing#testing-pages-layouts + +describe('AboutPage', () => { + it('renders successfully', () => { + expect(() => { + render() + }).not.toThrow() + }) +}) diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/AboutPage/AboutPage.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/AboutPage/AboutPage.tsx new file mode 100644 index 0000000000..b0476d6e22 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/pages/AboutPage/AboutPage.tsx @@ -0,0 +1,10 @@ +const AboutPage = () => { + return ( +

+ This site was created to demonstrate my mastery of Redwood: Look on my + works, ye mighty, and despair! +

+ ) +} + +export default AboutPage diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/BlogPostPage/BlogPostPage.routeHooks.ts b/__fixtures__/esm-fragment-test-project/web/src/pages/BlogPostPage/BlogPostPage.routeHooks.ts new file mode 100644 index 0000000000..e29f84e457 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/pages/BlogPostPage/BlogPostPage.routeHooks.ts @@ -0,0 +1,5 @@ +import { db } from '$api/src/lib/db.js' + +export async function routeParameters() { + return (await db.post.findMany({ take: 7 })).map((post) => ({ id: post.id })) +} diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/BlogPostPage/BlogPostPage.stories.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/BlogPostPage/BlogPostPage.stories.tsx new file mode 100644 index 0000000000..b8abecc304 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/pages/BlogPostPage/BlogPostPage.stories.tsx @@ -0,0 +1,17 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import BlogPostPage from './BlogPostPage' + +const meta: Meta = { + component: BlogPostPage, +} + +export default meta + +type Story = StoryObj + +export const Primary: Story = { + render: (args) => { + return + }, +} diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/BlogPostPage/BlogPostPage.test.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/BlogPostPage/BlogPostPage.test.tsx new file mode 100644 index 0000000000..1e1e4be5fb --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/pages/BlogPostPage/BlogPostPage.test.tsx @@ -0,0 +1,14 @@ +import { render } from '@cedarjs/testing/web' + +import BlogPostPage from './BlogPostPage' + +// Improve this test with help from the Redwood Testing Doc: +// https://redwoodjs.com/docs/testing#testing-pages-layouts + +describe('BlogPostPage', () => { + it('renders successfully', () => { + expect(() => { + render() + }).not.toThrow() + }) +}) diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/BlogPostPage/BlogPostPage.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/BlogPostPage/BlogPostPage.tsx new file mode 100644 index 0000000000..52aac7a3d6 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/pages/BlogPostPage/BlogPostPage.tsx @@ -0,0 +1,20 @@ +// import { Link, routes } from '@cedarjs/router' +import { Metadata } from '@cedarjs/web' + +type BlogPostPageProps = { + id: number +} + +import BlogPostCell from 'src/components/BlogPostCell' + +const BlogPostPage = ({ id }: BlogPostPageProps) => { + return ( + <> + + + + + ) +} + +export default BlogPostPage diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/Contact/ContactPage/ContactPage.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/Contact/ContactPage/ContactPage.tsx new file mode 100644 index 0000000000..9af63b0a3d --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/pages/Contact/ContactPage/ContactPage.tsx @@ -0,0 +1,11 @@ +import ContactCell from 'src/components/Contact/ContactCell' + +type ContactPageProps = { + id: number +} + +const ContactPage = ({ id }: ContactPageProps) => { + return +} + +export default ContactPage diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/Contact/ContactsPage/ContactsPage.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/Contact/ContactsPage/ContactsPage.tsx new file mode 100644 index 0000000000..7bc4048094 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/pages/Contact/ContactsPage/ContactsPage.tsx @@ -0,0 +1,7 @@ +import ContactsCell from 'src/components/Contact/ContactsCell' + +const ContactsPage = () => { + return +} + +export default ContactsPage diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/Contact/EditContactPage/EditContactPage.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/Contact/EditContactPage/EditContactPage.tsx new file mode 100644 index 0000000000..7241f71f7f --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/pages/Contact/EditContactPage/EditContactPage.tsx @@ -0,0 +1,11 @@ +import EditContactCell from 'src/components/Contact/EditContactCell' + +type ContactPageProps = { + id: number +} + +const EditContactPage = ({ id }: ContactPageProps) => { + return +} + +export default EditContactPage diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/Contact/NewContactPage/NewContactPage.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/Contact/NewContactPage/NewContactPage.tsx new file mode 100644 index 0000000000..2d4cc9274e --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/pages/Contact/NewContactPage/NewContactPage.tsx @@ -0,0 +1,7 @@ +import NewContact from 'src/components/Contact/NewContact' + +const NewContactPage = () => { + return +} + +export default NewContactPage diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/ContactUsPage/ContactUsPage.stories.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/ContactUsPage/ContactUsPage.stories.tsx new file mode 100644 index 0000000000..80eb779856 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/pages/ContactUsPage/ContactUsPage.stories.tsx @@ -0,0 +1,13 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import ContactUsPage from './ContactUsPage' + +const meta: Meta = { + component: ContactUsPage, +} + +export default meta + +type Story = StoryObj + +export const Primary: Story = {} diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/ContactUsPage/ContactUsPage.test.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/ContactUsPage/ContactUsPage.test.tsx new file mode 100644 index 0000000000..0731835b58 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/pages/ContactUsPage/ContactUsPage.test.tsx @@ -0,0 +1,14 @@ +import { render } from '@cedarjs/testing/web' + +import ContactUsPage from './ContactUsPage' + +// Improve this test with help from the Redwood Testing Doc: +// https://redwoodjs.com/docs/testing#testing-pages-layouts + +describe('ContactUsPage', () => { + it('renders successfully', () => { + expect(() => { + render() + }).not.toThrow() + }) +}) diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/ContactUsPage/ContactUsPage.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/ContactUsPage/ContactUsPage.tsx new file mode 100644 index 0000000000..de6488e1a7 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/pages/ContactUsPage/ContactUsPage.tsx @@ -0,0 +1,133 @@ +import { useState } from 'react' + +import { useForm } from 'react-hook-form' + +import { + Form, + TextField, + TextAreaField, + Submit, + FieldError, + Label, +} from '@cedarjs/forms' +import { useBlocker } from '@cedarjs/router' +import { useMutation } from '@cedarjs/web' +import { toast, Toaster } from '@cedarjs/web/toast' + +const CREATE_CONTACT = gql` + mutation CreateContactMutation($input: CreateContactInput!) { + createContact(input: $input) { + id + } + } +` + +const ContactUsPage = () => { + const formMethods = useForm() + const [isSubmitting, setIsSubmitting] = useState(false) + const blocker = useBlocker({ + when: formMethods.formState.isDirty && !isSubmitting, + }) + + const [create, { loading, error }] = useMutation(CREATE_CONTACT, { + onCompleted: () => { + toast.success('Thank you for your submission!') + }, + onError: (error) => { + toast.error(error.message) + }, + }) + + const onSubmit = async (data) => { + setIsSubmitting(true) + try { + await create({ variables: { input: data } }) + formMethods.reset(data) + } finally { + setIsSubmitting(false) + } + } + + return ( + <> + +
+ {blocker.state === 'BLOCKED' ? ( +
+ + +
+ ) : null} + + + + + + + + + + + + + + + Save + + + + ) +} + +export default ContactUsPage diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/DoublePage/DoublePage.stories.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/DoublePage/DoublePage.stories.tsx new file mode 100644 index 0000000000..adb222dbce --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/pages/DoublePage/DoublePage.stories.tsx @@ -0,0 +1,13 @@ +import type { Meta, StoryObj } from '@storybook/react' + +import DoublePage from './DoublePage' + +const meta: Meta = { + component: DoublePage, +} + +export default meta + +type Story = StoryObj + +export const Primary: Story = {} diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/DoublePage/DoublePage.test.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/DoublePage/DoublePage.test.tsx new file mode 100644 index 0000000000..b308591403 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/pages/DoublePage/DoublePage.test.tsx @@ -0,0 +1,14 @@ +import { render } from '@cedarjs/testing/web' + +import DoublePage from './DoublePage' + +// Improve this test with help from the Redwood Testing Doc: +// https://redwoodjs.com/docs/testing#testing-pages-layouts + +describe('DoublePage', () => { + it('renders successfully', () => { + expect(() => { + render() + }).not.toThrow() + }) +}) diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/DoublePage/DoublePage.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/DoublePage/DoublePage.tsx new file mode 100644 index 0000000000..75aef1171e --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/pages/DoublePage/DoublePage.tsx @@ -0,0 +1,39 @@ +import { Metadata } from '@cedarjs/web' + +import test from './test.png' + +const DoublePage = () => { + return ( + <> + + +

DoublePage

+

+ This page exists to make sure we don't regress on{' '} + + #7757 + +

+

For RW#7757 it needs to be a page that is not wrapped in a Set

+

+ We also use this page to make sure we don't regress on{' '} + + #317 + +

+ Test + + ) +} + +export default DoublePage diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/DoublePage/test.png b/__fixtures__/esm-fragment-test-project/web/src/pages/DoublePage/test.png new file mode 100644 index 0000000000..1232ba73ab Binary files /dev/null and b/__fixtures__/esm-fragment-test-project/web/src/pages/DoublePage/test.png differ diff --git a/__fixtures__/esm-fragment-test-project/web/src/pages/FatalErrorPage/FatalErrorPage.tsx b/__fixtures__/esm-fragment-test-project/web/src/pages/FatalErrorPage/FatalErrorPage.tsx new file mode 100644 index 0000000000..8afcee8498 --- /dev/null +++ b/__fixtures__/esm-fragment-test-project/web/src/pages/FatalErrorPage/FatalErrorPage.tsx @@ -0,0 +1,57 @@ +// This page will be rendered when an error makes it all the way to the top of the +// application without being handled by a Javascript catch statement or React error +// boundary. +// +// You can modify this page as you wish, but it is important to keep things simple to +// avoid the possibility that it will cause its own error. If it does, Redwood will +// still render a generic error page, but your users will prefer something a bit more +// thoughtful :) + +// This import will be automatically removed when building for production +import { DevFatalErrorPage } from '@cedarjs/web/dist/components/DevFatalErrorPage' + +export default DevFatalErrorPage || + (() => ( +
+