Skip to content

Commit

Permalink
updating branch with main
Browse files Browse the repository at this point in the history
  • Loading branch information
RuthShryock committed Nov 26, 2024
2 parents 6c60768 + 0474aed commit 43ce879
Show file tree
Hide file tree
Showing 109 changed files with 1,444 additions and 3,230 deletions.
49 changes: 23 additions & 26 deletions .github/workflows/npm-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,8 @@ jobs:
- name: Add "Node ${{ steps.resolved-node-version.outputs.NODE_VERSION }}" to summary
run: echo "${{ matrix.node-version }} → **${{ steps.resolved-node-version.outputs.NODE_VERSION }}**" >> "$GITHUB_STEP_SUMMARY"

# Set up Chrome, for the unit tests
- uses: browser-actions/setup-chrome@latest
- run: chrome --version

# Cache node_modules, keyed on os, node version, package-lock, and patches
# Cache: Use cache for node_modules
# Keyed on os, node version, package-lock, and patches
- uses: actions/cache@v4
name: Check for cached node_modules
id: cache-nodemodules
Expand All @@ -47,37 +44,37 @@ jobs:
path: node_modules
key: ${{ runner.os }}-build-${{ env.cache-name }}-node-v${{ steps.resolved-node-version.outputs.NODE_VERSION }}-${{ hashFiles('**/package-lock.json', 'patches/**/*.patch') }}

# Cache hit: node_modules is copied from a previous run. Run copy-fonts
- if: steps.cache-nodemodules.outputs.cache-hit == 'true'
name: Run copy-fonts (if using cached node_modules)
# Cache hit: If the cache key matches,
# /node_modules/ will have been copied from a previous run.
# (Run the post-install step, `npm run copy-fonts`)
- name: Run copy-fonts (if using cached node_modules)
if: steps.cache-nodemodules.outputs.cache-hit == 'true'
run: npm run copy-fonts

# Cache miss: Run npm install, which does copy-fonts as post-install step
- if: steps.cache-nodemodules.outputs.cache-hit != 'true'
name: Install JavaScript dependencies (npm install)
# Cache miss: If node_modules has not been cached,
# `npm install`
# (This includes `npm run copy-fonts` as post-install step)
- name: Install JavaScript dependencies (npm install)
if: steps.cache-nodemodules.outputs.cache-hit != 'true'
run: npm install

# Build the app!
# Check that the full build succeeds
- name: Build Prod
run: SKIP_TS_CHECK=true npm run build

# Run TypeScript Checks and ESLint
- name: Check TypeScript # Separated for visibility
# Check for TypeScript errors
- name: Check TypeScript
run: npm run check-types

# Check for ESLint messages (errors only)
- name: Check ESLint, errors only
run: npm run lint -- --quiet

# Unit Tests
- name: Build Tests
run: npx webpack --config webpack/test.config.js
# Run the Unit test suite (formbuilder and helpers)
- name: Run unit tests and xlform tests
run: npx jest --config ./jsapp/jest/unit.config.ts --ci

- name: Run Tests, with mocha-chrome
run: npx mocha-chrome test/tests.html --chrome-launcher.connectionPollInterval=5000
# This step takes less than 1 minute if it succeeds, but will hang for
# 6 hours if it fails with 'No inspectable targets'
# Timeout early to make it easier to manually re-run jobs.
# Tracking issue: https://github.com/kobotoolbox/kpi/issues/4337
timeout-minutes: 1
# Run the Jest test suite (React components)
- name: Run component tests with Jest
run: npx jest --config ./jsapp/jest/jest.config.ts --ci

- name: Run components tests with Jest
run: npm run jest
44 changes: 44 additions & 0 deletions jsapp/jest/coffeeTransformer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
const coffeescript = require('coffeescript');
const createCacheKeyFunction = require('@jest/create-cache-key-function').default;
/**
* @typedef {import('@jest/transform').SyncTransformer} SyncTransformer
* @typedef {import('@jest/transform').TransformedSource} TransformedSource
*/

/**
* Transform CoffeeScript files for Jest
* See: https://jestjs.io/docs/code-transformation
*
* @implements { SyncTransformer }
*/
module.exports = {
/**
* Process coffee files
*
* @param {string} sourceText
* @param {string} filename
* @returns {TransformedSource}
*/
process(sourceText, filename) {
const {js, sourceMap, v3SourceMap } = coffeescript.compile(
sourceText,
// ☕ CoffeeScript 1.12.7 compiler options
{
// 📜 For source maps
filename,
sourceMap: true,

// 📦 Same default as coffee-loader
bare: true,
}
);
return {
code: js,
map: JSON.parse(v3SourceMap),
};
},

getCacheKey: createCacheKeyFunction(
[__filename, require.resolve('coffeescript')],
),
};
17 changes: 17 additions & 0 deletions jsapp/jest/setupUnitTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import chai from 'chai';
import $ from 'jquery';

// Polyfill global fetch (for Node 20 and older)
import 'whatwg-fetch';

// Add global t() mock (see /static/js/global_t.js)
global.t = (str: string) => str;

// @ts-expect-error: ℹ️ Add chai global for BDD-style tests
global.chai = chai;

// @ts-expect-error: ℹ️ Use chai's version of `expect`
global.expect = chai.expect;

// @ts-expect-error: ℹ️ Add jQuery globals for xlform code
global.jQuery = global.$ = $;
58 changes: 58 additions & 0 deletions jsapp/jest/unit.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import type {Config} from 'jest';
import {defaults} from 'jest-config';

// Config to run ☕ unit tests using the Jest runner
//
// To run the unit tests: 🏃
//
// npx jest --config ./jsapp/jest/unit.config.ts
//

const config: Config = {
// Naming convention (*.tests.*)
testMatch: ['**/?(*.)+(tests).(js|jsx|ts|tsx|es6|coffee)'],

// Where to find tests. <rootDir> = 'kpi/jsapp/jest'
roots: [
'<rootDir>/../js/', // unit tests 🛠️ 'jsapp/js/**/*.tests.{ts,es6}'
'<rootDir>/../../test/', // xlform/coffee ☕ 'test/**/*.tests.coffee'
],

// Where to resolve module imports
moduleNameMapper: {
// ℹ️ same aliases as in webpack.common.js (module.resolve.alias)
'^jsapp/(.+)$': '<rootDir>/../$1', // 📁 'jsapp/*'
'^js/(.*)$': '<rootDir>/../js/$1', // 📁 'js/*'
'^test/(.*)$': '<rootDir>/../../test/$1', // 📁 'test/*'
'^utils$': '<rootDir>/../js/utils', // 📄 'utils'
// 🎨 mock all CSS modules imported (styles.root = 'root')
'\\.(css|scss)$': 'identity-obj-proxy',
},

// Extensions to try in order (for import statements with no extension)
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'es6', 'coffee'],

// Transformers (SWC for JS/TS, CoffeeScript for .coffee)
transform: {
'^.+\\.(js|jsx|ts|tsx|es6)$': '@swc/jest',
'^.+\\.coffee$': '<rootDir>/coffeeTransformer.js',
},

// Exclude these files, even if they contain tests
testPathIgnorePatterns: [
'test/xlform/integration.tests.coffee$', // 📄 skipped in `ee98aebe631b`
...defaults.testPathIgnorePatterns, // 📦 exclude '/node_modules/'
],

// Set up test environment
testEnvironment: 'jsdom',

// Make Chai and jQuery globals available in the test environment
setupFilesAfterEnv: ['<rootDir>/setupUnitTest.ts'],

// Appearance options (for console output)
verbose: true,
displayName: {name: 'UNIT', color: 'black'},
};

export default config;
8 changes: 5 additions & 3 deletions jsapp/js/account/accountSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ import subscriptionStore from 'js/account/subscriptionStore';
import envStore from 'js/envStore';
import useWhenStripeIsEnabled from 'js/hooks/useWhenStripeIsEnabled.hook';
import {ACCOUNT_ROUTES} from 'js/account/routes.constants';
import {useOrganizationQuery} from './stripe.api';
import {OrganizationUserRole} from './stripe.types';
import {
useOrganizationQuery,
OrganizationUserRole,
} from 'js/account/organization/organizationQuery';
import {useFeatureFlag, FeatureFlag} from 'js/featureFlags';
import {getSimpleMMOLabel} from './organizations/organizations.utils';
import {getSimpleMMOLabel} from './organization/organization.utils';
import LoadingSpinner from 'js/components/common/loadingSpinner';

interface AccountNavLinkProps {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@ import useWhen from 'js/hooks/useWhen.hook';
import subscriptionStore from 'js/account/subscriptionStore';
import type {
Price,
Organization,
Product,
SubscriptionInfo,
OneTimeAddOn,
} from 'js/account/stripe.types';
import {isAddonProduct} from 'js/account/stripe.utils';
import styles from './addOnList.module.scss';
import {OneTimeAddOnRow} from 'js/account/add-ons/oneTimeAddOnRow.component';
import {OneTimeAddOnRow} from 'jsapp/js/account/addOns/oneTimeAddOnRow.component';
import type {BadgeColor} from 'jsapp/js/components/common/badge';
import Badge from 'jsapp/js/components/common/badge';
import {formatDate} from 'js/utils';
import {OneTimeAddOnsContext} from 'jsapp/js/account/useOneTimeAddonList.hook';
import {FeatureFlag, useFeatureFlag} from 'jsapp/js/featureFlags';
import type {Organization} from 'js/account/organization/organizationQuery';

/**
* A table of add-on products along with dropdowns to purchase them.
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import styles from 'js/account/add-ons/addOnList.module.scss';
import styles from 'js/account/addOns/addOnList.module.scss';
import React, {useMemo, useState} from 'react';
import type {
Organization,
Product,
SubscriptionInfo,
} from 'js/account/stripe.types';
Expand All @@ -10,6 +9,7 @@ import BillingButton from 'js/account/plans/billingButton.component';
import {postCheckout, postCustomerPortal} from 'js/account/stripe.api';
import {useDisplayPrice} from 'js/account/plans/useDisplayPrice.hook';
import {isChangeScheduled} from 'js/account/stripe.utils';
import type {Organization} from 'js/account/organization/organizationQuery';

interface OneTimeAddOnRowProps {
products: Product[];
Expand Down
4 changes: 2 additions & 2 deletions jsapp/js/account/billingContextProvider.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@ import { OneTimeAddOnsContext, useOneTimeAddOns } from './useOneTimeAddonList.ho
import {UsageContext, useUsage} from 'js/account/usage/useUsage.hook';
import {ProductsContext, useProducts} from 'js/account/useProducts.hook';
import sessionStore from 'js/stores/session';
import {useOrganizationQuery} from 'js/account/stripe.api';
import {useOrganizationQuery} from 'js/account/organization/organizationQuery';

export const BillingContextProvider = (props: {children: ReactNode}) => {
const orgQuery = useOrganizationQuery();

if (!sessionStore.isLoggedIn) {
return <>{props.children}</>;
}

const usage = useUsage(orgQuery.data?.id || null);
const products = useProducts();
const oneTimeAddOns = useOneTimeAddOns();
Expand Down
102 changes: 102 additions & 0 deletions jsapp/js/account/organization/MembersRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Libraries
import React from 'react';

// Partial components
import PaginatedQueryUniversalTable from 'js/universalTable/paginatedQueryUniversalTable.component';
import LoadingSpinner from 'js/components/common/loadingSpinner';
import Avatar from 'js/components/common/avatar';
import Badge from 'jsapp/js/components/common/badge';

// Stores, hooks and utilities
import {formatTime} from 'js/utils';
import {useOrganizationQuery} from './organizationQuery';
import useOrganizationMembersQuery from './membersQuery';

// Constants and types
import type {OrganizationMember} from './membersQuery';

// Styles
import styles from './membersRoute.module.scss';

export default function MembersRoute() {
const orgQuery = useOrganizationQuery();

if (!orgQuery.data?.id) {
return (
<LoadingSpinner />
);
}

return (
<div className={styles.membersRouteRoot}>
<header className={styles.header}>
<h2 className={styles.headerText}>{t('Members')}</h2>
</header>

<PaginatedQueryUniversalTable<OrganizationMember>
queryHook={useOrganizationMembersQuery}
columns={[
{
key: 'user__extra_details__name',
label: t('Name'),
cellFormatter: (member: OrganizationMember) => (
<Avatar
size='m'
username={member.user__username}
isUsernameVisible
email={member.user__email}
// We pass `undefined` for the case it's an empty string
fullName={member.user__extra_details__name || undefined}
/>
),
size: 360,
},
{
key: 'invite',
label: t('Status'),
size: 120,
cellFormatter: (member: OrganizationMember) => {
if (member.invite?.status) {
return member.invite.status;
} else {
return <Badge color='light-green' size='s' label={t('Active')} />;
}
return null;
},
},
{
key: 'date_joined',
label: t('Date added'),
size: 140,
cellFormatter: (member: OrganizationMember) => formatTime(member.date_joined),
},
{
key: 'role',
label: t('Role'),
size: 120,
},
{
key: 'user__has_mfa_enabled',
label: t('2FA'),
size: 90,
cellFormatter: (member: OrganizationMember) => {
if (member.user__has_mfa_enabled) {
return <Badge size='s' color='light-blue' icon='check' />;
}
return <Badge size='s' color='light-storm' icon='minus' />;
},
},
{
// We use `url` here, but the cell would contain interactive UI
// element
key: 'url',
label: '',
size: 64,
// TODO: this will be added soon
cellFormatter: () => (' '),
},
]}
/>
</div>
);
}
7 changes: 7 additions & 0 deletions jsapp/js/account/organization/OrganizationSettingsRoute.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import React from 'react';

export default function OrganizationSettingsRoute() {
return (
<div>Organization settings view to be implemented</div>
);
}
Loading

0 comments on commit 43ce879

Please sign in to comment.