documentation icon indicating copy to clipboard operation
documentation copied to clipboard

Strapi 5 + typescript + jest === broken

Open corasaurus-hex opened this issue 1 year ago • 5 comments

Bug report

Required System information

  • Node.js version: v20.16.0
  • NPM version: 10.8.1 (yarn 1.22.22)
  • Strapi version: 5.0.0-rc.8
  • Database: sqlite
  • Operating system: macOS Sonoma 14.5
  • Is your project Javascript or Typescript: TypeScript
❯ yarn strapi report
yarn run v1.22.22
$ strapi report
Launched In: 141 ms
Environment: development
OS: darwin-arm64
Strapi Version: 5.0.0-rc.8
Node/Yarn Version: yarn/1.22.22 npm/? node/v20.16.0 darwin arm64
Edition: Community
Database: sqlite
[2024-08-04 18:40:08.311] info: Shutting down Strapi
[2024-08-04 18:40:08.312] info: Strapi has been shut down
✨  Done in 1.69s.

Describe the bug

Jest tests fail to run because Strapi won't load .ts config files.

Steps to reproduce the behavior

I published an example app that shows the issue to save you time, but I took the following steps to create the app:

  1. I set up a new Strapi project using:
npx create-strapi-app@rc --skip-cloud --typescript --use-yarn --dbclient sqlite strapi-typescript-test-bug-repro
  1. I then added Jest and its support libraries:
yarn add --dev jest ts-jest @types/jest
  1. I initialized the Jest config using:
yarn ts-jest config:init
  1. I then added the testPathIgnorePatterns to the Jest config as shown in the Strapi jest setup guide.

  2. I added a strapi.ts helper file as explained in the Strapi jest setup guide. I converted it to TypeScript and used the new createStrapi function as well.

  3. I added a config/env/test/database.ts and a config/env/production/database.ts file.

  4. I added the following lines to my .env file:

DATABASE_FILENAME=.tmp/data.db
TEST_DATABASE_FILENAME=.tmp/test_data.db
  1. I added tests/app.test.ts as described in the Strapi jest setup guide. This file has been converted to TypeScript as well.

  2. Run tests with yarn test.

Expand this to see the test output...
❯ yarn test
yarn run v1.22.22
$ jest --forceExit --detectOpenHandles
  console.warn
    Config file not loaded, extension must be one of .js,.json): admin.ts

       6 | export const setupStrapi = async () => {
       7 |   if (!instance) {
    >  8 |     await createStrapi().load();
         |                       ^
       9 |     instance = strapi;
      10 |
      11 |     instance.server.mount();

      at logWarning (node_modules/@strapi/core/src/configuration/config-loader.ts:57:11)
      at node_modules/@strapi/core/src/configuration/config-loader.ts:76:7
          at Array.reduce (<anonymous>)
      at loadConfigDir (node_modules/@strapi/core/src/configuration/config-loader.ts:65:32)
      at Module.loadConfigDir [as loadConfiguration] (node_modules/@strapi/core/src/configuration/index.ts:74:38)
      at new loadConfiguration (node_modules/@strapi/core/src/Strapi.ts:47:28)
      at createStrapi (node_modules/@strapi/core/src/index.ts:11:18)
      at setupStrapi (tests/helpers/strapi.ts:8:23)
      at Object.<anonymous> (tests/app.test.ts:4:20)

  console.warn
    Config file not loaded, extension must be one of .js,.json): api.ts

       6 | export const setupStrapi = async () => {
       7 |   if (!instance) {
    >  8 |     await createStrapi().load();
         |                       ^
       9 |     instance = strapi;
      10 |
      11 |     instance.server.mount();

      at logWarning (node_modules/@strapi/core/src/configuration/config-loader.ts:57:11)
      at node_modules/@strapi/core/src/configuration/config-loader.ts:76:7
          at Array.reduce (<anonymous>)
      at loadConfigDir (node_modules/@strapi/core/src/configuration/config-loader.ts:65:32)
      at Module.loadConfigDir [as loadConfiguration] (node_modules/@strapi/core/src/configuration/index.ts:74:38)
      at new loadConfiguration (node_modules/@strapi/core/src/Strapi.ts:47:28)
      at createStrapi (node_modules/@strapi/core/src/index.ts:11:18)
      at setupStrapi (tests/helpers/strapi.ts:8:23)
      at Object.<anonymous> (tests/app.test.ts:4:20)

  console.warn
    Config file not loaded, extension must be one of .js,.json): database.ts

       6 | export const setupStrapi = async () => {
       7 |   if (!instance) {
    >  8 |     await createStrapi().load();
         |                       ^
       9 |     instance = strapi;
      10 |
      11 |     instance.server.mount();

      at logWarning (node_modules/@strapi/core/src/configuration/config-loader.ts:57:11)
      at node_modules/@strapi/core/src/configuration/config-loader.ts:76:7
          at Array.reduce (<anonymous>)
      at loadConfigDir (node_modules/@strapi/core/src/configuration/config-loader.ts:65:32)
      at Module.loadConfigDir [as loadConfiguration] (node_modules/@strapi/core/src/configuration/index.ts:74:38)
      at new loadConfiguration (node_modules/@strapi/core/src/Strapi.ts:47:28)
      at createStrapi (node_modules/@strapi/core/src/index.ts:11:18)
      at setupStrapi (tests/helpers/strapi.ts:8:23)
      at Object.<anonymous> (tests/app.test.ts:4:20)

  console.warn
    Config file not loaded, extension must be one of .js,.json): middlewares.ts

       6 | export const setupStrapi = async () => {
       7 |   if (!instance) {
    >  8 |     await createStrapi().load();
         |                       ^
       9 |     instance = strapi;
      10 |
      11 |     instance.server.mount();

      at logWarning (node_modules/@strapi/core/src/configuration/config-loader.ts:57:11)
      at node_modules/@strapi/core/src/configuration/config-loader.ts:76:7
          at Array.reduce (<anonymous>)
      at loadConfigDir (node_modules/@strapi/core/src/configuration/config-loader.ts:65:32)
      at Module.loadConfigDir [as loadConfiguration] (node_modules/@strapi/core/src/configuration/index.ts:74:38)
      at new loadConfiguration (node_modules/@strapi/core/src/Strapi.ts:47:28)
      at createStrapi (node_modules/@strapi/core/src/index.ts:11:18)
      at setupStrapi (tests/helpers/strapi.ts:8:23)
      at Object.<anonymous> (tests/app.test.ts:4:20)

  console.warn
    Config file not loaded, extension must be one of .js,.json): plugins.ts

       6 | export const setupStrapi = async () => {
       7 |   if (!instance) {
    >  8 |     await createStrapi().load();
         |                       ^
       9 |     instance = strapi;
      10 |
      11 |     instance.server.mount();

      at logWarning (node_modules/@strapi/core/src/configuration/config-loader.ts:57:11)
      at node_modules/@strapi/core/src/configuration/config-loader.ts:76:7
          at Array.reduce (<anonymous>)
      at loadConfigDir (node_modules/@strapi/core/src/configuration/config-loader.ts:65:32)
      at Module.loadConfigDir [as loadConfiguration] (node_modules/@strapi/core/src/configuration/index.ts:74:38)
      at new loadConfiguration (node_modules/@strapi/core/src/Strapi.ts:47:28)
      at createStrapi (node_modules/@strapi/core/src/index.ts:11:18)
      at setupStrapi (tests/helpers/strapi.ts:8:23)
      at Object.<anonymous> (tests/app.test.ts:4:20)

  console.warn
    Config file not loaded, extension must be one of .js,.json): server.ts

       6 | export const setupStrapi = async () => {
       7 |   if (!instance) {
    >  8 |     await createStrapi().load();
         |                       ^
       9 |     instance = strapi;
      10 |
      11 |     instance.server.mount();

      at logWarning (node_modules/@strapi/core/src/configuration/config-loader.ts:57:11)
      at node_modules/@strapi/core/src/configuration/config-loader.ts:76:7
          at Array.reduce (<anonymous>)
      at loadConfigDir (node_modules/@strapi/core/src/configuration/config-loader.ts:65:32)
      at Module.loadConfigDir [as loadConfiguration] (node_modules/@strapi/core/src/configuration/index.ts:74:38)
      at new loadConfiguration (node_modules/@strapi/core/src/Strapi.ts:47:28)
      at createStrapi (node_modules/@strapi/core/src/index.ts:11:18)
      at setupStrapi (tests/helpers/strapi.ts:8:23)
      at Object.<anonymous> (tests/app.test.ts:4:20)

  console.warn
    Config file not loaded, extension must be one of .js,.json): database.ts

       6 | export const setupStrapi = async () => {
       7 |   if (!instance) {
    >  8 |     await createStrapi().load();
         |                       ^
       9 |     instance = strapi;
      10 |
      11 |     instance.server.mount();

      at logWarning (node_modules/@strapi/core/src/configuration/config-loader.ts:57:11)
      at node_modules/@strapi/core/src/configuration/config-loader.ts:76:7
          at Array.reduce (<anonymous>)
      at loadConfigDir (node_modules/@strapi/core/src/configuration/config-loader.ts:65:32)
      at Module.loadConfigDir [as loadConfiguration] (node_modules/@strapi/core/src/configuration/index.ts:77:21)
      at new loadConfiguration (node_modules/@strapi/core/src/Strapi.ts:47:28)
      at createStrapi (node_modules/@strapi/core/src/index.ts:11:18)
      at setupStrapi (tests/helpers/strapi.ts:8:23)
      at Object.<anonymous> (tests/app.test.ts:4:20)

 FAIL  tests/app.test.ts
  ● strapi is defined

    TypeError: Cannot destructure property 'client' of 'db.config.connection' as it is undefined.

       6 | export const setupStrapi = async () => {
       7 |   if (!instance) {
    >  8 |     await createStrapi().load();
         |     ^
       9 |     instance = strapi;
      10 |
      11 |     instance.server.mount();

      at getDialect (node_modules/@strapi/database/src/dialects/index.ts:40:11)
      at new Database (node_modules/@strapi/database/src/index.ts:80:20)
      at node_modules/@strapi/core/src/Strapi.ts:275:11
      at Strapi.get (node_modules/@strapi/core/src/container.ts:27:35)
      at Strapi.get db [as db] (node_modules/@strapi/core/src/Strapi.ts:77:17)
      at Object.register (node_modules/@strapi/core/src/providers/registries.ts:38:12)
      at Strapi.register (node_modules/@strapi/core/src/Strapi.ts:400:13)
      at Strapi.load (node_modules/@strapi/core/src/Strapi.ts:387:5)
      at setupStrapi (tests/helpers/strapi.ts:8:5)
      at Object.<anonymous> (tests/app.test.ts:4:3)


  ● Test suite failed to run

    TypeError: Cannot destructure property 'client' of 'db.config.connection' as it is undefined.

      23 |
      24 |   // close the connection to the database before deletion
    > 25 |   await strapi.db.connection.destroy();
         |                ^
      26 |
      27 |   //delete test database after all tests have completed
      28 |   if (dbSettings?.connection?.filename) {

      at getDialect (node_modules/@strapi/database/src/dialects/index.ts:40:11)
      at new Database (node_modules/@strapi/database/src/index.ts:80:20)
      at node_modules/@strapi/core/src/Strapi.ts:275:11
      at Strapi.get (node_modules/@strapi/core/src/container.ts:27:35)
      at Strapi.get db [as db] (node_modules/@strapi/core/src/Strapi.ts:77:17)
      at cleanupStrapi (tests/helpers/strapi.ts:25:16)
      at Object.<anonymous> (tests/app.test.ts:8:22)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        1.697 s, estimated 2 s
Ran all test suites.
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

As you can see, there are a lot of warnings like Config file not loaded, extension must be one of .js,.json): database.ts and then the tests fails because something internal to Strapi can't destructure config values.

Interestingly, if I pass the value { distDir: "dist" } to createStrapi in strapi.ts, and run yarn build before I run yarn test, the tests pass. Presumably this is because it's running the tests from the dist dir and yarn build is transpiling those files to .js files.

Expected behavior

I would expect there to be some way to run tests under TypeScript with Strapi that didn't require a build first. This is also broken under Strapi 4 in the same way and I was trying out Strapi 5 to see if I could make it work.

Screenshots

N/A

Code snippets

N/A

Additional context

I'm really happy Strapi is getting better TypeScript support! I'd love if it could get better testing support as well. Thanks so much for the great software!!

corasaurus-hex avatar Aug 05 '24 00:08 corasaurus-hex

I'm not sure this is a documentation issue, @derrickmehaffy. Strapi is specifically dynamically loading .js and .json configuration files at runtime to configure the server: https://github.com/strapi/strapi/blob/v5.0.0-rc.10/packages/core/core/src/configuration/config-loader.ts#L5 -- I believe it would need to be a core framework change to also allow loading .ts files.

corasaurus-hex avatar Aug 18 '24 22:08 corasaurus-hex

Unless, I guess, the docs say to use something else, like vitest or something like it that can incrementally compile and run tests.

corasaurus-hex avatar Aug 18 '24 22:08 corasaurus-hex

This issue has been mentioned on Strapi Community Forum. There might be relevant details there:

https://forum.strapi.io/t/how-can-i-add-vitest-to-a-strapi-5-app/41255/1

strapi-bot avatar Aug 18 '24 22:08 strapi-bot

running

I can make it it run by add jest.config.ts and ts-jest, but I still get this error message: TypeError: Cannot destructure property 'client' of 'db.config.connection' as it is undefined

const {defaults: tsjPreset} = require('ts-jest/presets');
const {pathsToModuleNameMapper} = require('ts-jest/dist');
const {compilerOptions} = require('./tsconfig.test.json');


export default async () => {
  return {
    roots: ["<rootDir>/src/", "<rootDir>/config/", "<rootDir>/tests/"],
    testMatch: [
      "**/__tests__/**/*.+(ts|tsx|js)",
      "**/?(*.)+(spec|test).+(ts|tsx|js)"
    ],
    testEnvironment: 'node',
    transform: {
      ...tsjPreset.transform
    },
    testPathIgnorePatterns: [
      "<rootDir>/__mocks__/*",
      "node_modules",
      "\\.tmp",
      "\\.cache",
      "<rootDir>.*/public",
      "<rootDir>/dist/*",
    ],
    moduleNameMapper: {
      ...pathsToModuleNameMapper(compilerOptions.paths, {prefix: '<rootDir>/'})
    },
    setupFilesAfterEnv: [
      "<rootDir>/tests/helpers/strapi.ts" // Path to your setup file
    ],
    testTimeout: 60000,
    verbose: false,
  };
};

saltict avatar Sep 05 '24 08:09 saltict

Hello,

Currently, if you follow the doc, the main problem I encountered was the database with /env/test/database.ts seems not to work. I'm able to fix this problem, you need to configure in your config/database, your database for testing (ex. sqlite file) and define differents modes like this :

import path from "path";

export default ({ env }) => {
  let client = env("DATABASE_CLIENT", "postgres");

  if (process.env.NODE_ENV === "test") {
    client = "sqlite";
  }

  const connections = {
    postgres: {
      connection: {
        connectionString: env("DATABASE_URL"),
        host: env("DATABASE_HOST", "localhost"),
        port: env.int("DATABASE_PORT", 5432),
        database: env("DATABASE_NAME", "strapi"),
        user: env("DATABASE_USERNAME", "strapi"),
        password: env("DATABASE_PASSWORD", "strapi"),
        ssl: env.bool("DATABASE_SSL", false) && {
          key: env("DATABASE_SSL_KEY", undefined),
          cert: env("DATABASE_SSL_CERT", undefined),
          ca: env("DATABASE_SSL_CA", undefined),
          capath: env("DATABASE_SSL_CAPATH", undefined),
          cipher: env("DATABASE_SSL_CIPHER", undefined),
          rejectUnauthorized: env.bool(
            "DATABASE_SSL_REJECT_UNAUTHORIZED",
            true
          ),
        },
        schema: env("DATABASE_SCHEMA", "public"),
      },
      pool: {
        min: env.int("DATABASE_POOL_MIN", 2),
        max: env.int("DATABASE_POOL_MAX", 10),
      },
    },
    sqlite: {
      connection: {
        filename: path.join(
          __dirname,
          "..",
          "..",
          env("DATABASE_FILENAME", ".tmp/data.db")
        ),
      },
      useNullAsDefault: true,
      debug: false,
    },
  };

  return {
    connection: {
      client,
      ...connections[client],
      acquireConnectionTimeout: env.int("DATABASE_CONNECTION_TIMEOUT", 60000),
    },
  };
}

My test suite has passed successfully, but I have come across a new bug : {A6625EFF-C1A8-4F66-BC53-50B2807130AF}

Bastiendsp avatar Oct 18 '24 09:10 Bastiendsp