wallabyjs / public

Repository for Wallaby.js questions and issues
http://wallabyjs.com
759 stars 45 forks source link

Auto-configure angular-electron project #2406

Closed soupman99 closed 4 years ago

soupman99 commented 4 years ago

I'm trying to add wallaby to an existing starter project.

https://github.com/maximegris/angular-electron

When I try to use auto configuration I get this error

[Error] Failed to load configuration file: Automatic Angular CLI configuration error: None of your projects are configured with @angular-devkit/build-angular:karma builder for test architect.  [Error] We've also tried to automatically configure Wallaby.js for other frameworks  [Error] Automatic Jest configuration error: Module jest-cli is not found in '/Users/justin/Desktop/_temp/angular-electron'. 

How would I go about incorporating wallaby?

smcenlly commented 4 years ago

Wallaby's automatic configuration for Angular currently only supports the @angular-devkit/build-angular:karma builder, whereas your project is using @angular-builders/custom-webpack:karma.

To configure Wallaby for your project, you may follow our Angular CLI tutorial and in the Select configuration mode section, select Manual (instead of Automatic).

  1. Add the wallaby-webpack dependency, npm install wallaby-webpack --save-dev
  2. Copy the Wallaby configuration to wallaby.js in your project root, and change project.architect.test.builder === '@angular-devkit/build-angular:karma' to project.architect.test.builder === '@angular-builders/custom-webpack:karma'.
  3. Restart Wallaby, specifying to run from configuration file instead of automatic configuration.

The working wallaby.js configuration for your project can be found below. Please note that it is using Chrome, not electron to run your tests. You may also update your configuration to run your tests using electron (described in our docs)

module.exports = function(wallaby) {
  const wallabyWebpack = require('wallaby-webpack');
  const path = require('path');
  const fs = require('fs');

  const specPattern = '/**/*spec.ts';
  const angularConfig = require('./angular.json');

  const projects = Object.keys(angularConfig.projects).map(key => {
    return { name: key, ...angularConfig.projects[key] };
  }).filter(project => project.sourceRoot)
    .filter(project => project.projectType !== 'application' || 
                       (project.architect &&
                        project.architect.test &&
                        project.architect.test.builder === '@angular-builders/custom-webpack:karma'));

  const applications = projects.filter(project => project.projectType === 'application');
  const libraries = projects.filter(project => project.projectType === 'library');

  const tsConfigFile = projects
    .map(project => path.join(__dirname, project.root, 'tsconfig.spec.json'))
    .find(tsConfig => fs.existsSync(tsConfig));

  const tsConfigSpec = tsConfigFile ? JSON.parse(fs.readFileSync(tsConfigFile)) : {};

  const compilerOptions = Object.assign(require('./tsconfig.json').compilerOptions, tsConfigSpec.compilerOptions);
  compilerOptions.emitDecoratorMetadata = true;

  return {
    files: [
      { pattern: path.basename(__filename), load: false, instrument: false },
      ...projects.map(project => ({
        pattern: project.sourceRoot + '/**/*.+(ts|js|css|less|scss|sass|styl|html|json|svg)',
        load: false
      })),
      ...projects.map(project => ({
        pattern: project.sourceRoot + specPattern,
        ignore: true
      })),
      ...projects.map(project => ({
        pattern: project.sourceRoot + '/**/*.d.ts',
        ignore: true
      }))
    ],

    tests: [
      ...projects.map(project => ({
        pattern: project.sourceRoot + specPattern,
        load: false
      }))
    ],

    testFramework: 'jasmine',

    compilers: {
      '**/*.ts': wallaby.compilers.typeScript({
        ...compilerOptions,
        getCustomTransformers: program => {
          return {
            before: [
              require('@ngtools/webpack/src/transformers/replace_resources').replaceResources(
                path => true,
                () => program.getTypeChecker(),
                false
              )
            ]
          };
        }
      })
    },

    preprocessors: {
      /* Initialize Test Environment for Wallaby */
      [path.basename(__filename)]: file => `
 import '@angular-devkit/build-angular/src/angular-cli-files/models/jit-polyfills';
 import 'zone.js/dist/zone-testing';
 import { getTestBed } from '@angular/core/testing';
 import { BrowserDynamicTestingModule,  platformBrowserDynamicTesting} from '@angular/platform-browser-dynamic/testing';

 getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());`
    },

    middleware: function(app, express) {
      const path = require('path');

      applications.forEach(application => {
        if (
          !application.architect ||
          !application.architect.test ||
          !application.architect.test.options ||
          !application.architect.test.options.assets
        ) {
          return;
        }

        application.architect.test.options.assets.forEach(asset => {
          if (asset && !asset.glob) {
            // Only works for file assets (not globs)
            // (https://github.com/angular/angular-cli/blob/master/docs/documentation/stories/asset-configuration.md#project-assets)
            app.use(asset.slice(application.sourceRoot.length), express.static(path.join(__dirname, asset)));
          }
        });
      });
    },

    env: {
      kind: 'chrome'
    },

    postprocessor: wallabyWebpack({
      entryPatterns: [
        ...applications
          .map(project => project.sourceRoot + '/polyfills.js')
          .filter(polyfills => fs.existsSync(path.join(__dirname, polyfills.replace(/js$/, 'ts')))),
        path.basename(__filename),
        ...projects.map(project => project.sourceRoot + specPattern.replace(/ts$/, 'js'))
      ],

      module: {
        rules: [
          { test: /\.css$/, loader: ['raw-loader'] },
          { test: /\.html$/, loader: 'raw-loader' },
          {
            test: /\.ts$/,
            loader: '@ngtools/webpack',
            include: /node_modules/,
            query: { tsConfigPath: 'tsconfig.json' }
          },
          { test: /\.styl$/, loaders: ['raw-loader', 'stylus-loader'] },
          { test: /\.less$/, loaders: ['raw-loader', { loader: 'less-loader' }] },
          {
            test: /\.scss$|\.sass$/,
            loaders: [{ loader: 'raw-loader' }, { loader: 'sass-loader', options: { implementation: require('sass') } }]
          },
          { test: /\.(jpg|png|svg)$/, loader: 'raw-loader' }
        ]
      },

      resolve: {
        extensions: ['.js', '.ts'],
        modules: [
          wallaby.projectCacheDir,
          ...(projects.length ? projects.filter(project => project.root)
            .map(project => path.join(wallaby.projectCacheDir, project.root)) : []),
          ...(projects.length ? projects.filter(project => project.sourceRoot)
            .map(project => path.join(wallaby.projectCacheDir,project.sourceRoot)) : []),
          'node_modules'
        ],
        alias: libraries.reduce((result, project) => {
          const alias =  project.name.replace(/([a-zA-Z])(?=[A-Z])/g, '$1-').toLowerCase();
          result[alias] = path.join(wallaby.projectCacheDir, project.sourceRoot, 'public-api');
          return result;
        }, {})
      }
    }),

    setup: function() {
      window.__moduleBundler.loadTests();
    }
  };
};
soupman99 commented 4 years ago

Thanks for this! It works great. An additional question that I've uncovered is how to incorporate tests for my actual electron code as well but I can't seem to get it working.

I've created a repo here: https://github.com/soupman99/wallaby-electron-angular

When I run wallaby my test in electron/test/example.spec.ts don't actually get executed.

However, when I remove let testClass = new TestElectron() the code executes as expected.

Is this due to wallaby not being able to find electron/test/example.ts ?

smcenlly commented 4 years ago

Your existing Angular tests are running with Wallaby using Webpack, which means that if you want to run your tests in the electron directory at the same time, you'll need to configure the electron files to be bundled with Webpack and executed the same way as the Angular project. You can read a little more about how to configure Wallaby to run with webpack here but I have updated your configuration for you to make this work (see below).

The configuration that I had to change/fix was: 1) Set load: false to all source files and tests processed by webpack (except external files), as they should not be loaded in browser, not by Wallaby. 2) Update patterns to include source files in files section, and exclude .spec files. (note: I also excluded main.ts as I figured it's not meant to be run/used when testing). 3) Update webpack entryPatterns to run your test on startup.

Wallaby.js Configuration

module.exports = function(wallaby) {
    const wallabyWebpack = require('wallaby-webpack');
    const path = require('path');
    const fs = require('fs');

    const specPattern = '/**/*spec.+(ts|js)';
    const angularConfig = require('./angular.json');

    const projects = Object.keys(angularConfig.projects).map(key => {
      return { name: key, ...angularConfig.projects[key] };
    }).filter(project => project.sourceRoot)
      .filter(project => project.projectType !== 'application' || 
                         (project.architect &&
                          project.architect.test &&
                          project.architect.test.builder === '@angular-builders/custom-webpack:karma'));

    const applications = projects.filter(project => project.projectType === 'application');
    const libraries = projects.filter(project => project.projectType === 'library');

    const tsConfigFile = projects
      .map(project => path.join(__dirname, project.root, 'tsconfig.spec.json'))
      .find(tsConfig => fs.existsSync(tsConfig));

    const tsConfigSpec = tsConfigFile ? JSON.parse(fs.readFileSync(tsConfigFile)) : {};

    const compilerOptions = Object.assign(require('./tsconfig.json').compilerOptions, tsConfigSpec.compilerOptions);
    compilerOptions.emitDecoratorMetadata = true;

    return {
      files: [
        { pattern: path.basename(__filename), load: false, instrument: false },
        ...projects.map(project => ({
          pattern: project.sourceRoot + '/**/*.+(ts|js|css|less|scss|sass|styl|html|json|svg|pug)',
          load: false
        })),
        ...projects.map(project => ({
          pattern: project.sourceRoot + specPattern,
          ignore: true
        })),
        ...projects.map(project => ({
          pattern: project.sourceRoot + '/**/*.d.ts',
          ignore: true
        })),

-       {pattern: '/electron/**/*.spec.+(ts|js)', ignore: true, load:true},
+       { pattern: "electron/**/*.+(ts|js)", ignore: false, load: false },
+       { pattern: "electron/**/*spec.+(ts|js)", ignore: true },
+       { pattern: "electron/main.ts", ignore: true },
      ],

      tests: [
        ...projects.map(project => ({
          pattern: project.sourceRoot + specPattern,
          load: false
        })),

-       {pattern: '/electron/**/*.spec.+(ts|js)', load: true},       
+       { pattern: "electron/**/*spec.+(ts|js)", load: false },
      ],

      testFramework: 'jasmine',

      compilers: {
        '**/*.ts': wallaby.compilers.typeScript({
          ...compilerOptions,
          getCustomTransformers: program => {
            return {
              before: [
                require('@ngtools/webpack/src/transformers/replace_resources').replaceResources(
                  path => true,
                  () => program.getTypeChecker(),
                  false
                )
              ]
            };
          }
        })
      },

      preprocessors: {
        /* Initialize Test Environment for Wallaby */
        [path.basename(__filename)]: file => `
   import '@angular-devkit/build-angular/src/angular-cli-files/models/jit-polyfills';
   import 'zone.js/dist/zone-testing';
   import { getTestBed } from '@angular/core/testing';
   import { BrowserDynamicTestingModule,  platformBrowserDynamicTesting} from '@angular/platform-browser-dynamic/testing';

   getTestBed().initTestEnvironment(BrowserDynamicTestingModule, platformBrowserDynamicTesting());`
      },

      middleware: function(app, express) {
        const path = require('path');

        applications.forEach(application => {
          if (
            !application.architect ||
            !application.architect.test ||
            !application.architect.test.options ||
            !application.architect.test.options.assets
          ) {
            return;
          }

          application.architect.test.options.assets.forEach(asset => {
            if (asset && !asset.glob) {
              // Only works for file assets (not globs)
              // (https://github.com/angular/angular-cli/blob/master/docs/documentation/stories/asset-configuration.md#project-assets)
              app.use(asset.slice(application.sourceRoot.length), express.static(path.join(__dirname, asset)));
            }
          });
        });
      },

      env: {
        kind: 'chrome'
      },

      postprocessor: wallabyWebpack({
        entryPatterns: [
          ...applications
            .map(project => project.sourceRoot + '/polyfills.js')
            .filter(polyfills => fs.existsSync(path.join(__dirname, polyfills.replace(/js$/, 'ts')))),
          path.basename(__filename),
+         'electron/**/*spec.+(ts|js)',
          ...projects.map(project => project.sourceRoot + specPattern.replace(/ts$/, 'js')),
          'electron/**/*spec.+(ts|js)'
        ],

        module: {
          rules: [
            { test: /\.css$/, loader: ['raw-loader'] },
            { test: /\.html$/, loader: 'raw-loader' },
            {
              test: /\.ts$/,
              loader: '@ngtools/webpack',
              include: /node_modules/,
              query: { tsConfigPath: 'tsconfig.json' }
            },
            { test: /\.styl$/, loaders: ['raw-loader', 'stylus-loader'] },
            { test: /\.less$/, loaders: ['raw-loader', { loader: 'less-loader' }] },
            {
              test: /\.scss$|\.sass$/,
              loaders: [{ loader: 'raw-loader' }, { loader: 'sass-loader', options: { implementation: require('sass') } }]
            },
            {test: /\.(pug|jade)$/, exclude: /\.(include|partial)\.(pug|jade)$/, loaders: ['apply-loader', { loader: 'pug-loader' }]}, 
            // {test: /\.(include|partial)\.(pug|jade)$/, loader: 'pug-loader'},

            { test: /\.(jpg|png|svg)$/, loader: 'raw-loader' }
          ]
        },

        resolve: {
          extensions: ['.js', '.ts'],
          modules: [
            wallaby.projectCacheDir,
            ...(projects.length ? projects.filter(project => project.root)
              .map(project => path.join(wallaby.projectCacheDir, project.root)) : []),
            ...(projects.length ? projects.filter(project => project.sourceRoot)
              .map(project => path.join(wallaby.projectCacheDir,project.sourceRoot)) : []),
            'node_modules'
          ],
          alias: libraries.reduce((result, project) => {
            const alias =  project.name.replace(/([a-zA-Z])(?=[A-Z])/g, '$1-').toLowerCase();
            result[alias] = path.join(wallaby.projectCacheDir, project.sourceRoot, 'public-api');
            return result;
          }, {})
        }
      }),

      setup: function() {
        window.__moduleBundler.loadTests();
      }
    };
  };
soupman99 commented 4 years ago

Sorry to keep being "that guy"

I've switched over to jest and it seems like I'm very close to having things resolved once and for all.

I've got jest working with angular-cli but not with wallaby.

My electron tests pass when I run wallaby but my angular tests fail with the error Zone is needed for the async() test helper but could not be found.

I added src/wallaby-test.ts but still no luck. Am I missing an import somewhere?

I've updated my example repo: https://github.com/soupman99/wallaby-electron-angular

smcenlly commented 4 years ago

If you're using jest, the best way to get Wallaby up and running is to make sure that npx jest works from your project root.

Currently you're relying on Angular's ng command to run your tests which is configuring jest for you under the covers.

I edited ./node_modules/@angular-builders/jest/dist/index.js to dump the jest configuration that is being passed to jest by the angular jest test runner so that I could create a jest.config.js file in your project root.

jest.config.js

module.exports = {
  globals: {
    "ts-jest": {
      stringifyContentPathRegex: "\\.html$",
      astTransformers: [
        "jest-preset-angular/build/InlineFilesTransformer",
        "jest-preset-angular/build/StripStylesTransformer",
      ],
    },
  },
  preset: "jest-preset-angular",
  testURL: "https://github.com/@angular-cli-builders",
  setupFilesAfterEnv: [
    "<rootDir>/node_modules/@angular-builders/jest/dist/jest-config/setup.js",
  ],
  moduleNameMapper: {
    "\\.(jpg|jpeg|png)$":
      "<rootDir>/node_modules/@angular-builders/jest/dist/jest-config/mock-module.js",
  },
  testMatch: ["<rootDir>/**/*(*.)@(spec|test).[tj]s?(x)"],
};

After creating the jest configuration, delete your wallaby.js configuration file and switch to use our automatic configuration feature. At that point everything will work for you.

soupman99 commented 4 years ago

@smcenlly that works like a charm! Thanks you so much!