Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
version: 2.1

# E2E check for the built add-on.
#
# Runs on every PR (including Dependabot) so dependency bumps can be gated on
# the add-on still installing, building, loading and wiring IPC correctly. The
# Local runtime and Electron are mocked; the real @getflywheel/local-components
# and React are exercised. See test/.
jobs:
e2e:
docker:
- image: cimg/node:22.15
steps:
- checkout
- restore_cache:
keys:
- v1-deps-{{ checksum "yarn.lock" }}
- v1-deps-
- run:
name: Install dependencies
command: yarn install --frozen-lockfile
- save_cache:
key: v1-deps-{{ checksum "yarn.lock" }}
paths:
- node_modules
- run:
name: Build add-on
command: yarn build
- run:
name: Run e2e tests
command: yarn test --ci

workflows:
e2e:
jobs:
- e2e
6 changes: 6 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@
# @getflywheel/local-engineers will be requested for
# review when someone opens a pull request.
* @getflywheel/local-engineers

# Dependency files managed by Dependabot — excluded from CODEOWNERS so
# Dependabot PRs can be approved and merged with GITHUB_TOKEN only.
# Note: .github/workflows/ is intentionally NOT excluded (security requirement).
package.json
yarn.lock
12 changes: 2 additions & 10 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,22 @@ updates:
cooldown:
default-days: 7
groups:
all-non-major-updates:
all-dependencies:
patterns:
- "*"
update-types:
- "minor"
- "patch"
ignore:
- dependency-name: "*"
update-types:
- "version-update:semver-major"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
cooldown:
default-days: 7
groups:
all-non-major-updates:
all-dependencies:
patterns:
- "*"
update-types:
- "minor"
- "patch"
ignore:
- dependency-name: "*"
update-types:
- "version-update:semver-major"
27 changes: 27 additions & 0 deletions .github/workflows/dependabot-automation.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Dependabot Automation

on: pull_request

permissions:
contents: write
pull-requests: write

jobs:
dependabot:
runs-on: ubuntu-latest
if: ${{github.actor == 'dependabot[bot]'}}
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@25dd0e34f4fe68f24cc83900b1fe3fe149efef98 # v3.1.0
with:
github-token: "${{secrets.GITHUB_TOKEN}}"
- name: Approve and enable auto-merge for a PR
if: ${{steps.metadata.outputs.update-type == 'version-update:semver-minor' || steps.metadata.outputs.update-type == 'version-update:semver-patch'}}
env:
PR_URL: ${{github.event.pull_request.html_url}}
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
run: |
set -eu
gh pr review --approve "$PR_URL"
gh pr merge --auto --squash "$PR_URL"
6 changes: 6 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -1 +1,7 @@
.github/
test/
jest.config.js
**/*.test.js
*.log
coverage/
.nyc_output/
22 changes: 22 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// E2E harness for the built add-on.
//
// The `@getflywheel/local` runtime only exists inside the Local desktop app, so
// we map it (and `electron`) to lightweight mocks and load the *compiled* add-on
// from `lib/`. The real `@getflywheel/local-components`, `react` and `react-dom`
// are exercised, which is what makes this useful as a dependency-bump gate: if a
// bump breaks the add-on's build, imports, rendering or IPC wiring, it fails.
module.exports = {
testEnvironment: 'jsdom',
testMatch: ['<rootDir>/test/**/*.test.js'],
// The compiled output is plain CommonJS; no transforms are needed, which
// keeps the suite fast and dependency-free.
transform: {},
clearMocks: true,
setupFiles: ['<rootDir>/test/setup.js'],
moduleNameMapper: {
'^electron$': '<rootDir>/test/mocks/electron.js',
'^@getflywheel/local$': '<rootDir>/test/mocks/local.js',
'^@getflywheel/local/renderer$': '<rootDir>/test/mocks/local-renderer.js',
'^@getflywheel/local/main$': '<rootDir>/test/mocks/local-main.js',
},
};
8 changes: 7 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,19 @@
"scripts": {
"build": "tsc",
"watch": "yarn run build --watch",
"prepare": "npm run build"
"prepare": "npm run build",
"test": "jest",
"test:e2e": "jest"
},
"devDependencies": {
"@getflywheel/local": "^9.2.4",
"@testing-library/dom": "^10.4.2",
"@testing-library/react": "^16.3.3",
"@types/classnames": "^2.3.4",
"@types/prop-types": "^15.7.14",
"@types/react": "^19.0.12",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"typescript": "^5.8.2"
},
"peerDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion sonar-project.properties
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@ sonar.sources=src,style.css,assets
sonar.exclusions=**/*.test.*,**/*.spec.*,node_modules/**,lib/**,dist/**,build/**,coverage/**,.nyc_output/**

# Paths to test source directories (relative paths)
sonar.tests=src
sonar.tests=test
sonar.test.inclusions=**/*.test.*,**/*.spec.*
35 changes: 35 additions & 0 deletions test/main.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* E2E test for the add-on's compiled main-process entry point (lib/main.js).
*
* It drives the exact artifact that ships to Local: loading it against a mocked
* Local runtime and asserting that the `update-site-notes` IPC channel is wired
* up and persists notes through `siteData.updateSite`.
*/
const electron = require('electron');
const { siteData } = require('./mocks/local-main.js');
const registerMain = require('../lib/main.js').default;

describe('main process (built lib/main.js)', () => {
it('registers the update-site-notes IPC handler on load', () => {
registerMain({ electron });

expect(electron.ipcMain.on).toHaveBeenCalledWith(
'update-site-notes',
expect.any(Function),
);
});

it('persists incoming notes to the site', () => {
registerMain({ electron });

const handler = electron.ipcMain.__handlers.get('update-site-notes');
const notes = [{ body: 'Remember this', date: new Date().toISOString(), pinned: false }];

handler({}, 'site-123', notes);

expect(siteData.updateSite).toHaveBeenCalledWith('site-123', {
id: 'site-123',
notes,
});
});
});
20 changes: 20 additions & 0 deletions test/mocks/electron.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Minimal stand-in for the `electron` module, exposing just enough of
// `ipcMain` / `ipcRenderer` for the add-on and letting specs assert on IPC.
const ipcMainHandlers = new Map();
const ipcRendererSend = jest.fn();

const ipcMain = {
on: jest.fn((channel, handler) => {
ipcMainHandlers.set(channel, handler);
}),
removeListener: jest.fn(),
__handlers: ipcMainHandlers,
};

const ipcRenderer = {
send: ipcRendererSend,
on: jest.fn(),
removeListener: jest.fn(),
};

module.exports = { ipcMain, ipcRenderer };
11 changes: 11 additions & 0 deletions test/mocks/local-main.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Stand-in for `@getflywheel/local/main`. `getServiceContainer().cradle.siteData`
// is how the add-on persists notes, so specs can assert directly against it.
const siteData = {
updateSite: jest.fn(),
};

const getServiceContainer = jest.fn(() => ({
cradle: { siteData },
}));

module.exports = { getServiceContainer, siteData };
10 changes: 10 additions & 0 deletions test/mocks/local-renderer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Stand-in for `@getflywheel/local/renderer`. The add-on imports `confirm` from
// here for the delete-note dialog.
const confirm = jest.fn(() => Promise.resolve());

const HooksRenderer = {
addContent: jest.fn(),
doContent: jest.fn(),
};

module.exports = { confirm, HooksRenderer };
3 changes: 3 additions & 0 deletions test/mocks/local.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// The base `@getflywheel/local` package only provides type definitions; nothing
// from it is required at runtime by this add-on.
module.exports = {};
79 changes: 79 additions & 0 deletions test/renderer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* E2E test for the add-on's compiled renderer entry point (lib/renderer.js).
*
* The real `@getflywheel/local-components` and React are rendered into jsdom;
* only the Local app runtime and Electron are mocked. This catches dependency
* bumps that break hook registration, component rendering or IPC syncing.
*/
const React = require('react');
const { render, screen, fireEvent, cleanup } = require('@testing-library/react');
const electron = require('electron');
const registerRenderer = require('../lib/renderer.js').default;

function createHooks() {
const registered = {};

return {
registered,
addContent(hook, callback) {
(registered[hook] = registered[hook] || []).push(callback);
},
};
}

function renderSiteOverview(site) {
const hooks = createHooks();
registerRenderer({ React, hooks });

const siteInfoOverview = hooks.registered.SiteInfoOverview[0];
return render(siteInfoOverview(site));
}

afterEach(cleanup);

describe('renderer process (built lib/renderer.js)', () => {
it('registers the stylesheet and SiteInfoOverview hooks', () => {
const hooks = createHooks();

registerRenderer({ React, hooks });

expect(hooks.registered.stylesheets).toHaveLength(1);
expect(hooks.registered.SiteInfoOverview).toHaveLength(1);
});

it('renders the empty notes sidebar for a site', () => {
renderSiteOverview({ id: 'site-1', notes: [] });

expect(screen.getByText('Notes')).toBeTruthy();
expect(screen.getByText('+ Add Note')).toBeTruthy();
});

it('renders existing notes for a site', () => {
const date = new Date('2024-01-01T00:00:00Z');

renderSiteOverview({
id: 'site-1',
notes: [{ body: 'Existing note body', date, pinned: false }],
});

expect(screen.getByText('Existing note body')).toBeTruthy();
});

it('adds a note and syncs it to the main process over IPC', () => {
renderSiteOverview({ id: 'site-1', notes: [] });

fireEvent.click(screen.getByText('+ Add Note'));

const textarea = screen.getByPlaceholderText('Add a note...');
fireEvent.change(textarea, { target: { value: 'My first note' } });
fireEvent.keyPress(textarea, { key: 'Enter', charCode: 13, keyCode: 13 });

expect(electron.ipcRenderer.send).toHaveBeenCalledTimes(1);

const [channel, siteId, notes] = electron.ipcRenderer.send.mock.calls[0];
expect(channel).toBe('update-site-notes');
expect(siteId).toBe('site-1');
expect(notes).toHaveLength(1);
expect(notes[0]).toMatchObject({ body: 'My first note', pinned: false });
});
});
39 changes: 39 additions & 0 deletions test/setup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// jsdom is missing a few browser/Node APIs that @getflywheel/local-components
// and its transitive deps touch on import / render. Providing inert stubs keeps
// the harness focused on the add-on itself.
const { TextDecoder, TextEncoder } = require('util');

if (typeof global.TextEncoder === 'undefined') {
global.TextEncoder = TextEncoder;
}

if (typeof global.TextDecoder === 'undefined') {
global.TextDecoder = TextDecoder;
}

if (typeof window !== 'undefined') {
if (!window.matchMedia) {
window.matchMedia = () => ({
matches: false,
media: '',
onchange: null,
addListener() {},
removeListener() {},
addEventListener() {},
removeEventListener() {},
dispatchEvent() {
return false;
},
});
}

if (!window.ResizeObserver) {
window.ResizeObserver = class ResizeObserver {
observe() {}

unobserve() {}

disconnect() {}
};
}
}
5 changes: 4 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@
"target": "es2015"
},
"exclude": [
"node_modules"
"node_modules",
"src/**/*.test.js",
"src/**/*.spec.js",
"test"
],
"include": [
"src"
Expand Down
Loading
Loading