├── dist
└── .gitkeep
├── .nvmrc
├── src
├── lib
│ ├── process
│ │ ├── debug
│ │ │ └── .gitkeep
│ │ ├── runners
│ │ │ ├── index.ts
│ │ │ ├── yarn.ts
│ │ │ └── npm.ts
│ │ ├── queue.ts
│ │ ├── shell.ts
│ │ ├── errors.ts
│ │ ├── factory.ts
│ │ ├── pool.ts
│ │ └── search.ts
│ ├── logger
│ │ ├── levels.ts
│ │ ├── format.ts
│ │ ├── main.ts
│ │ ├── renderer.ts
│ │ └── preload.ts
│ ├── frameworks
│ │ ├── progress.ts
│ │ ├── index.ts
│ │ ├── phpunit
│ │ │ └── suite.ts
│ │ ├── phpunit-10
│ │ │ └── suite.ts
│ │ ├── emitter.ts
│ │ ├── factory.ts
│ │ └── sort.ts
│ ├── crash
│ │ └── reporter.ts
│ ├── reporters
│ │ ├── phpunit
│ │ │ └── src
│ │ │ │ ├── Printer.php
│ │ │ │ ├── 70-80
│ │ │ │ └── Printer.php
│ │ │ │ ├── Util.php
│ │ │ │ └── Lode.php
│ │ └── phpunit-10
│ │ │ ├── src
│ │ │ ├── Subscriber
│ │ │ │ ├── TestFailedSubscriber.php
│ │ │ │ ├── TestPassedSubscriber.php
│ │ │ │ ├── TestErroredSubscriber.php
│ │ │ │ ├── TestSkippedSubscriber.php
│ │ │ │ ├── TestFinishedSubscriber.php
│ │ │ │ ├── ApplicationFinishedSubscriber.php
│ │ │ │ ├── TestErrorTriggeredSubscriber.php
│ │ │ │ ├── TestConsideredRiskySubscriber.php
│ │ │ │ ├── TestNoticeTriggeredSubscriber.php
│ │ │ │ ├── TestMarkedIncompleteSubscriber.php
│ │ │ │ ├── TestWarningTriggeredSubscriber.php
│ │ │ │ ├── TestPhpNoticeTriggeredSubscriber.php
│ │ │ │ ├── TestPhpWarningTriggeredSubscriber.php
│ │ │ │ ├── TestDeprecationTriggeredSubscriber.php
│ │ │ │ ├── TestPhpunitErrorTriggeredSubscriber.php
│ │ │ │ ├── TestSuiteStartedSubscriber.php
│ │ │ │ ├── TestPhpDeprecationTriggeredSubscriber.php
│ │ │ │ ├── TestPhpunitWarningTriggeredSubscriber.php
│ │ │ │ ├── Subscriber.php
│ │ │ │ └── TestPhpunitDeprecationTriggeredSubscriber.php
│ │ │ ├── ExtensionHook.php
│ │ │ ├── Util.php
│ │ │ ├── Status.php
│ │ │ └── Lode.php
│ │ │ └── bootstrap.php
│ ├── themes
│ │ └── index.ts
│ ├── state
│ │ └── project.ts
│ └── helpers
│ │ └── paths.ts
├── styles
│ ├── blocks
│ │ ├── diff.scss
│ │ ├── tables.scss
│ │ ├── preferences.scss
│ │ ├── icons.scss
│ │ ├── test-information.scss
│ │ ├── cta.scss
│ │ ├── license.scss
│ │ ├── pre.scss
│ │ ├── scrollable.scss
│ │ ├── object.scss
│ │ ├── console.scss
│ │ ├── breadcrumbs.scss
│ │ ├── draggable.scss
│ │ ├── test.scss
│ │ ├── terms.scss
│ │ ├── filename.scss
│ │ ├── contents.scss
│ │ ├── about.scss
│ │ ├── feedback.scss
│ │ ├── meta.scss
│ │ ├── trace.scss
│ │ ├── code.scss
│ │ ├── ansi.scss
│ │ ├── loading.scss
│ │ ├── results.scss
│ │ ├── test-result.scss
│ │ ├── project.scss
│ │ └── selective.scss
│ ├── images
│ │ ├── error.png
│ │ ├── error@2x.png
│ │ ├── success.png
│ │ ├── success@2x.png
│ │ ├── octocat-spinner-32.gif
│ │ ├── octocat-spinner-16px.gif
│ │ └── octocat-spinner-32-EAF2F5.gif
│ ├── highlight
│ │ ├── dark.scss
│ │ └── light.scss
│ ├── typography.scss
│ ├── globals.scss
│ ├── vendor.scss
│ ├── animations.scss
│ ├── mixins.scss
│ ├── app.scss
│ ├── definitions.scss
│ └── functions.scss
├── preload
│ ├── index.ts
│ └── lode.ts
├── renderer
│ ├── plugins
│ │ ├── unproxy.js
│ │ ├── translation.js
│ │ ├── strings.js
│ │ ├── durations.ts
│ │ ├── alerts.ts
│ │ └── modals.js
│ ├── components
│ │ ├── Pane.vue
│ │ ├── Draggable.vue
│ │ ├── modals
│ │ │ ├── mixins
│ │ │ │ ├── modal.js
│ │ │ │ └── confirm.js
│ │ │ ├── RemoveProject.vue
│ │ │ ├── ResetSettings.vue
│ │ │ ├── Terms.vue
│ │ │ ├── ProjectLoadingFailed.vue
│ │ │ ├── RemoveFramework.vue
│ │ │ ├── RemoveRepository.vue
│ │ │ ├── RunningUnderTranslation.vue
│ │ │ ├── ConfirmSwitchProject.vue
│ │ │ ├── Licenses.vue
│ │ │ └── About.vue
│ │ ├── ProjectLoader.vue
│ │ ├── Parameters.vue
│ │ ├── Icon.vue
│ │ ├── Duration.vue
│ │ ├── Split.vue
│ │ ├── MetaTable.vue
│ │ ├── KeyValue.vue
│ │ ├── mixins
│ │ │ ├── HasFrameworkMenu.js
│ │ │ └── HasFile.js
│ │ ├── Indicator.vue
│ │ ├── App.vue
│ │ ├── ModalController.vue
│ │ ├── Snippet.vue
│ │ ├── Collapsible.vue
│ │ ├── Filename.vue
│ │ └── Console.vue
│ ├── store
│ │ ├── modules
│ │ │ ├── settings.js
│ │ │ ├── ledger.js
│ │ │ ├── tabs.js
│ │ │ ├── status.js
│ │ │ ├── filters.js
│ │ │ ├── alert.js
│ │ │ ├── theme.js
│ │ │ ├── expand.js
│ │ │ └── modals.js
│ │ └── index.js
│ ├── directives
│ │ └── markdown.js
│ └── helpers
│ │ └── validator.js
├── types
│ ├── shims.d.ts
│ └── vuex-shim.d.ts
├── main
│ ├── menu
│ │ ├── index.ts
│ │ ├── test-menu.ts
│ │ └── file-menu.ts
│ └── index.dev.ts
└── index.ejs
├── workflow
├── release-after.js
├── run-dev.js
├── clean-build.js
├── icons.js
├── reporters
│ ├── phpunit.js
│ └── webpack.jest.config.js
├── webpack.preload.config.js
├── release-before.js
├── app-info.js
├── clean.js
├── decipher.js
├── webpack.main.config.js
├── build.js
├── cypress.js
└── webpack.base.config.js
├── support
├── release-notes.md
├── assets
│ ├── dmg-bg.png
│ ├── dmg-bg.tiff
│ └── dmg-bg@2x.png
├── release-notes.example.md
└── entitlements.mac.plist
├── static
└── icons
│ ├── 32x32.png
│ ├── 128x128.png
│ ├── 256x256.png
│ ├── 512x512.png
│ ├── 1024x1024.png
│ └── gem.svg
├── tests
├── fixtures
│ ├── framework
│ │ ├── project.json
│ │ ├── types.json
│ │ ├── ledger.json
│ │ ├── jest
│ │ │ └── 26
│ │ │ │ └── options.json
│ │ ├── repositories.json
│ │ └── phpunit
│ │ │ └── 8.0
│ │ │ └── options.json
│ └── process
│ │ ├── 22.json
│ │ ├── 24.json
│ │ ├── decoded.json
│ │ ├── 4.json
│ │ ├── 2.json
│ │ ├── 3.json
│ │ ├── 1.json
│ │ ├── 9.json
│ │ ├── 21.json
│ │ ├── 20.json
│ │ ├── 27.json
│ │ ├── 28.json
│ │ ├── 16.json
│ │ ├── 17.json
│ │ ├── 19.json
│ │ ├── 8.json
│ │ ├── 25.json
│ │ ├── 6.json
│ │ ├── 7.json
│ │ ├── 18.json
│ │ ├── 26.json
│ │ ├── 5.json
│ │ ├── 23.json
│ │ ├── 29.json
│ │ ├── 13.json
│ │ ├── 11.json
│ │ ├── 12.json
│ │ ├── 10.json
│ │ ├── 14.json
│ │ └── 15.json
├── mocks
│ ├── setup.js
│ └── electron.js
├── setup.js
├── cypress
│ ├── support
│ │ ├── index.js
│ │ ├── ipc.js
│ │ ├── process.js
│ │ └── assertions.js
│ ├── e2e
│ │ └── theme.js
│ └── config.ts
├── lib
│ ├── helpers
│ │ └── durations.spec.js
│ ├── frameworks
│ │ ├── factory.spec.js
│ │ └── emitter.spec.js
│ └── process
│ │ └── pool.spec.js
└── renderer
│ ├── components
│ └── TestInformation.spec.js
│ └── directives
│ └── markdown.spec.js
├── .eslintignore
├── .editorconfig
├── .gitignore
├── .github
├── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
└── workflows
│ └── codeql-analysis.yml
├── .stylelintrc.json
├── babel.config.js
├── .vscode
└── launch.json
├── tsconfig.json
├── LICENSE
└── jest.config.js
/dist/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 20.11.1
2 |
--------------------------------------------------------------------------------
/src/lib/process/debug/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/workflow/release-after.js:
--------------------------------------------------------------------------------
1 | // Nothing to do.
2 |
--------------------------------------------------------------------------------
/support/release-notes.md:
--------------------------------------------------------------------------------
1 | New:
2 | - Preliminary support for PHPUnit 10+
3 |
--------------------------------------------------------------------------------
/src/lib/logger/levels.ts:
--------------------------------------------------------------------------------
1 | export type LogLevel = 'error' | 'warn' | 'info' | 'debug'
2 |
--------------------------------------------------------------------------------
/src/styles/blocks/diff.scss:
--------------------------------------------------------------------------------
1 | .diff {
2 | margin-bottom: var(--spacing-double);
3 | }
4 |
--------------------------------------------------------------------------------
/static/icons/32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lodeapp/lode/HEAD/static/icons/32x32.png
--------------------------------------------------------------------------------
/static/icons/128x128.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lodeapp/lode/HEAD/static/icons/128x128.png
--------------------------------------------------------------------------------
/static/icons/256x256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lodeapp/lode/HEAD/static/icons/256x256.png
--------------------------------------------------------------------------------
/static/icons/512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lodeapp/lode/HEAD/static/icons/512x512.png
--------------------------------------------------------------------------------
/support/assets/dmg-bg.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lodeapp/lode/HEAD/support/assets/dmg-bg.png
--------------------------------------------------------------------------------
/workflow/run-dev.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const init = require('./runners').init
4 | init()
5 |
--------------------------------------------------------------------------------
/src/styles/images/error.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lodeapp/lode/HEAD/src/styles/images/error.png
--------------------------------------------------------------------------------
/static/icons/1024x1024.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lodeapp/lode/HEAD/static/icons/1024x1024.png
--------------------------------------------------------------------------------
/support/assets/dmg-bg.tiff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lodeapp/lode/HEAD/support/assets/dmg-bg.tiff
--------------------------------------------------------------------------------
/src/styles/blocks/tables.scss:
--------------------------------------------------------------------------------
1 | .markdown-body {
2 | table {
3 | width: 100%;
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/styles/images/error@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lodeapp/lode/HEAD/src/styles/images/error@2x.png
--------------------------------------------------------------------------------
/src/styles/images/success.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lodeapp/lode/HEAD/src/styles/images/success.png
--------------------------------------------------------------------------------
/support/assets/dmg-bg@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lodeapp/lode/HEAD/support/assets/dmg-bg@2x.png
--------------------------------------------------------------------------------
/src/styles/images/success@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lodeapp/lode/HEAD/src/styles/images/success@2x.png
--------------------------------------------------------------------------------
/tests/fixtures/framework/project.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "42",
3 | "name": "Biscuit",
4 | "active": null
5 | }
6 |
--------------------------------------------------------------------------------
/src/styles/images/octocat-spinner-32.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lodeapp/lode/HEAD/src/styles/images/octocat-spinner-32.gif
--------------------------------------------------------------------------------
/src/styles/images/octocat-spinner-16px.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lodeapp/lode/HEAD/src/styles/images/octocat-spinner-16px.gif
--------------------------------------------------------------------------------
/src/styles/images/octocat-spinner-32-EAF2F5.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/lodeapp/lode/HEAD/src/styles/images/octocat-spinner-32-EAF2F5.gif
--------------------------------------------------------------------------------
/tests/mocks/setup.js:
--------------------------------------------------------------------------------
1 | global.log = {
2 | debug: () => {},
3 | info: () => {},
4 | warn: () => {},
5 | error: () => {}
6 | }
7 |
--------------------------------------------------------------------------------
/src/lib/frameworks/progress.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * A ledger of run progress.
3 | */
4 | export type ProgressLedger = {
5 | run: number,
6 | total: number
7 | }
8 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /build/**
2 | /dist/**
3 | /static/**
4 | /src/lib/reporters/**
5 | /src/lib/process/debug/**
6 | /src/types/**
7 | /babel.config.js
8 | /workflow
9 |
--------------------------------------------------------------------------------
/tests/setup.js:
--------------------------------------------------------------------------------
1 | module.exports = async () => {
2 | // Force all tests to run in UTC timezone (i.e. same as our pipelines).
3 | process.env.TZ = 'UTC'
4 | }
5 |
--------------------------------------------------------------------------------
/tests/cypress/support/index.js:
--------------------------------------------------------------------------------
1 | import electron from '../../mocks/electron'
2 |
3 | import './ipc'
4 | import './process'
5 | import './assertions'
6 |
7 | window.electron = electron
8 |
--------------------------------------------------------------------------------
/tests/fixtures/process/22.json:
--------------------------------------------------------------------------------
1 | {
2 | "process": {
3 | "rawChunks": [
4 | "\u001b[1mStarting...\u001b[0m\n\u001b[1mEnded!\u001b[0m"
5 | ]
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/preload/index.ts:
--------------------------------------------------------------------------------
1 | import '@lib/logger/preload'
2 |
3 | import { contextBridge } from 'electron'
4 | import { Lode } from './lode'
5 |
6 | contextBridge.exposeInMainWorld('Lode', Lode)
7 |
--------------------------------------------------------------------------------
/src/lib/crash/reporter.ts:
--------------------------------------------------------------------------------
1 | import * as Sentry from '@sentry/electron'
2 |
3 | if (process.env.NODE_ENV !== 'development') {
4 | Sentry.init({
5 | dsn: __CRASH_URL__
6 | })
7 | }
8 |
--------------------------------------------------------------------------------
/src/renderer/plugins/unproxy.js:
--------------------------------------------------------------------------------
1 | export default class Unproxy {
2 | install (app) {
3 | app.config.globalProperties.$unproxy = value => JSON.parse(JSON.stringify(value))
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/src/styles/blocks/preferences.scss:
--------------------------------------------------------------------------------
1 | .preferences {
2 | .form-group {
3 | dl {
4 | dt {
5 | width: 180px;
6 | }
7 | }
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/styles/highlight/dark.scss:
--------------------------------------------------------------------------------
1 | @include dark-context {
2 | /* stylelint-disable-next-line no-invalid-position-at-import-rule */
3 | @import "node_modules/highlight.js/styles/atom-one-dark";
4 | }
5 |
--------------------------------------------------------------------------------
/src/lib/reporters/phpunit/src/Printer.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
--------------------------------------------------------------------------------
/tests/fixtures/framework/types.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "name": "Jest",
4 | "type": "jest",
5 | "command": "yarn test",
6 | "path": "",
7 | "proprietary": {}
8 | }
9 | ]
10 |
--------------------------------------------------------------------------------
/src/lib/process/runners/index.ts:
--------------------------------------------------------------------------------
1 | import { YarnProcess } from './yarn'
2 | import { NpmProcess } from './npm'
3 |
4 | export { YarnProcess }
5 | export { NpmProcess }
6 |
7 | export const Runners = [YarnProcess, NpmProcess]
8 |
--------------------------------------------------------------------------------
/src/styles/blocks/icons.scss:
--------------------------------------------------------------------------------
1 | i {
2 | display: inline-block;
3 | fill: currentcolor;
4 |
5 | &.rotate-90 {
6 | margin-right: 0 !important;
7 | transform: rotate(90deg);
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/tests/fixtures/process/24.json:
--------------------------------------------------------------------------------
1 | {
2 | "process": {
3 | "rawChunks": [
4 | "\n<<
2 |
3 |
4 |
5 |
14 |
--------------------------------------------------------------------------------
/src/renderer/components/modals/mixins/modal.js:
--------------------------------------------------------------------------------
1 | import Modal from '@/components/modals/Modal.vue'
2 |
3 | export default {
4 | components: {
5 | Modal
6 | },
7 | emits: ['hide'],
8 | methods: {
9 | close () {
10 | this.$emit('hide')
11 | }
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/renderer/components/ProjectLoader.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
14 |
--------------------------------------------------------------------------------
/src/styles/blocks/test-information.scss:
--------------------------------------------------------------------------------
1 | .test-information {
2 | font-size: 1.1rem;
3 |
4 | .heading {
5 | font-weight: var(--font-weight-semibold);
6 | white-space: nowrap;
7 | width: 1%;
8 | }
9 |
10 | td:not(.heading) {
11 | user-select: text;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | dist/*
3 | build/*
4 | node_modules/
5 | npm-debug.log
6 | npm-debug.log.*
7 | thumbs.db
8 | !.gitkeep
9 | src/main/lib/process/debug/*.json
10 | src/lib/reporters/phpunit/bootstrap
11 | support/icons
12 | tests/coverage
13 | tests/cypress/videos
14 | tests/cypress/screenshots
15 | yarn-error.log
16 |
--------------------------------------------------------------------------------
/src/renderer/plugins/strings.js:
--------------------------------------------------------------------------------
1 | import BaseStrings from '@lib/helpers/strings'
2 |
3 | export default class Strings {
4 | constructor (locale = 'en-US') {
5 | this.locale = locale
6 | }
7 |
8 | install (app) {
9 | app.config.globalProperties.$string = new BaseStrings(this.locale)
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/styles/blocks/cta.scss:
--------------------------------------------------------------------------------
1 | .cta {
2 | animation: fade-in-top .4s forwards;
3 |
4 | code {
5 | background-color: var(--secondary-background-color-dark);
6 | padding: 2px;
7 | }
8 |
9 | .btn {
10 | + .btn {
11 | margin-left: var(--spacing);
12 | }
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/styles/typography.scss:
--------------------------------------------------------------------------------
1 | h1,
2 | h2,
3 | h3,
4 | h4,
5 | h5,
6 | h6 {
7 | &.text-muted {
8 | color: var(--color-fg-subtle);
9 | margin-bottom: var(--spacing-half);
10 | text-transform: uppercase;
11 | }
12 | }
13 |
14 | code {
15 | font-family: var(--font-family-monospace) !important;
16 | }
17 |
--------------------------------------------------------------------------------
/src/types/vuex-shim.d.ts:
--------------------------------------------------------------------------------
1 | import { ComponentCustomProperties } from 'vue'
2 | import { Store } from 'vuex'
3 |
4 | declare module '@vue/runtime-core' {
5 | // Declare your own store states.
6 | interface State {
7 | count: number
8 | }
9 |
10 | interface ComponentCustomProperties {
11 | $store: Store
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/renderer/store/modules/settings.js:
--------------------------------------------------------------------------------
1 | import { get } from 'lodash'
2 |
3 | export default {
4 | namespaced: true,
5 | state: {
6 | },
7 | getters: {
8 | value: state => key => {
9 | if (!key) {
10 | return state
11 | }
12 | return get(state, key)
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/tests/fixtures/process/decoded.json:
--------------------------------------------------------------------------------
1 | {
2 | "biscuit": "Hobnob",
3 | "ingredients": [
4 | "Rolled Oats",
5 | "Wholemeal Wheat Flour",
6 | "Sugar",
7 | "Vegetable Oil",
8 | "Partially Inverted Sugar Syrup",
9 | "Sodium Bicarbonate",
10 | "Ammonium Bicarbonate",
11 | "Salt"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/src/styles/blocks/license.scss:
--------------------------------------------------------------------------------
1 | .license {
2 | counter-increment: dependency;
3 |
4 | h5 {
5 | &::before {
6 | content: counters(dependency, ".") ". ";
7 | }
8 | }
9 |
10 | pre {
11 | cursor: text;
12 | margin: var(--spacing-half) 0 var(--spacing-double);
13 | user-select: text;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/renderer/components/Parameters.vue:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
7 |
18 |
--------------------------------------------------------------------------------
/src/renderer/store/index.js:
--------------------------------------------------------------------------------
1 | import { createStore } from 'vuex'
2 |
3 | // Load all modules automatically
4 | const context = require.context('@/store/modules', true, /\.js$/)
5 | const modules = {}
6 | context.keys().forEach((key) => {
7 | modules[key.replace(/^\.\/([aA0-zZ9]+)\.js$/, '$1')] = context(key).default
8 | })
9 |
10 | export default createStore({
11 | modules,
12 | strict: true
13 | })
14 |
--------------------------------------------------------------------------------
/workflow/reporters/phpunit.js:
--------------------------------------------------------------------------------
1 | const Path = require('path')
2 | const Fs = require('fs-extra')
3 |
4 | Fs.mkdirpSync(Path.join(__dirname, '../../static/reporters'))
5 | Fs.copySync(Path.join(__dirname, '../../src/lib/reporters/phpunit'), Path.join(__dirname, '../../static/reporters/phpunit'))
6 | Fs.copySync(Path.join(__dirname, '../../src/lib/reporters/phpunit-10'), Path.join(__dirname, '../../static/reporters/phpunit-10'))
7 |
--------------------------------------------------------------------------------
/src/styles/blocks/pre.scss:
--------------------------------------------------------------------------------
1 | pre {
2 | background-color: var(--secondary-background-color);
3 | border: 1px solid var(--primary-border-color);
4 | border-radius: 3px;
5 | font-family: var(--font-family-monospace);
6 | padding: var(--spacing);
7 | user-select: text;
8 | white-space: pre-wrap;
9 | word-break: break-word;
10 |
11 | &:empty {
12 | display: none;
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/styles/blocks/scrollable.scss:
--------------------------------------------------------------------------------
1 | @use "sass:color";
2 |
3 | .scrollable {
4 | .shadow {
5 | &::after {
6 | border-top: 1px solid var(--color-border-default);
7 | box-shadow: var(--color-shadow-small);
8 | content: "";
9 | height: 0;
10 | position: fixed;
11 | width: 100%;
12 | z-index: 1;
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/renderer/plugins/durations.ts:
--------------------------------------------------------------------------------
1 | import type { App } from 'vue'
2 | import BaseDurations from '@lib/helpers/durations'
3 |
4 | export default class Durations {
5 | private locale: string
6 |
7 | constructor (locale: string = 'en-US') {
8 | this.locale = locale
9 | }
10 |
11 | install (app: App) {
12 | app.config.globalProperties.$duration = new BaseDurations(this.locale)
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/lib/reporters/phpunit/src/70-80/Printer.php:
--------------------------------------------------------------------------------
1 |
2 | import { h } from 'vue'
3 | import octicons from '@primer/octicons'
4 |
5 | export default {
6 | name: 'Icon',
7 | props: {
8 | symbol: {
9 | type: String,
10 | required: true
11 | }
12 | },
13 | render () {
14 | return h('i', {
15 | innerHTML: octicons[this.symbol].toSVG()
16 | })
17 | }
18 | }
19 |
20 |
--------------------------------------------------------------------------------
/src/renderer/components/Duration.vue:
--------------------------------------------------------------------------------
1 |
2 | {{ formatted }}
3 |
4 |
5 |
21 |
--------------------------------------------------------------------------------
/support/release-notes.example.md:
--------------------------------------------------------------------------------
1 | New:
2 | - Group names with array items will be used as badges.
3 | - This is another array item (prefixed by a dash, parsed as YAML)
4 | Changed:
5 | - This is another group. Colons will not show.
6 | Fixed:
7 | - Also supports **markdown**
8 | Removed:
9 | - New, Changed, Fixed and Removed are categories that have custom styling
10 | Empty groups will be rendered as stand-alone notes (i.e. no badge prefix).
11 |
--------------------------------------------------------------------------------
/workflow/webpack.preload.config.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | process.env.BABEL_ENV = 'preload'
4 |
5 | const base = require('./webpack.base.config.js')
6 | const { merge } = require('webpack-merge')
7 | const path = require('path')
8 |
9 | const preloadConfig = merge(base, {
10 | target: 'electron-preload',
11 | entry: {
12 | preload: path.join(__dirname, '../src/preload/index.ts')
13 | }
14 | })
15 |
16 | module.exports = preloadConfig
17 |
--------------------------------------------------------------------------------
/src/styles/blocks/object.scss:
--------------------------------------------------------------------------------
1 | .object {
2 | > .key-value {
3 | margin-bottom: var(--spacing);
4 |
5 | > .key {
6 | color: var(--color-fg-subtler);
7 | font-weight: var(--font-weight-semibold);
8 | text-transform: uppercase;
9 | }
10 |
11 | > .value {
12 | user-select: text;
13 | }
14 | }
15 |
16 | > :last-child {
17 | margin-bottom: 0;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/lib/reporters/phpunit-10/src/Subscriber/TestFailedSubscriber.php:
--------------------------------------------------------------------------------
1 | reporter()->handleEvent($event);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/lib/reporters/phpunit-10/src/Subscriber/TestPassedSubscriber.php:
--------------------------------------------------------------------------------
1 | reporter()->handleEvent($event);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/lib/reporters/phpunit-10/src/Subscriber/TestErroredSubscriber.php:
--------------------------------------------------------------------------------
1 | reporter()->handleEvent($event);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/lib/reporters/phpunit-10/src/Subscriber/TestSkippedSubscriber.php:
--------------------------------------------------------------------------------
1 | reporter()->handleEvent($event);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/lib/reporters/phpunit-10/src/Subscriber/TestFinishedSubscriber.php:
--------------------------------------------------------------------------------
1 | reporter()->handleEvent($event);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/lib/logger/format.ts:
--------------------------------------------------------------------------------
1 | export function formatError (error: Error, title?: string): string {
2 | return title
3 | ? `${title}\n${error.name}: ${error.message}`
4 | : `${error.name}: ${error.message}`
5 | }
6 |
7 | export function formatLogMessage (message: string | object, error?: Error): string {
8 | if (typeof message === 'object') {
9 | message = JSON.stringify(message)
10 | }
11 | return error ? formatError(error, message) : message
12 | }
13 |
--------------------------------------------------------------------------------
/support/entitlements.mac.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | com.apple.security.cs.allow-jit
6 |
7 | com.apple.security.cs.allow-unsigned-executable-memory
8 |
9 | com.apple.security.cs.allow-dyld-environment-variables
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/lib/reporters/phpunit-10/src/Subscriber/ApplicationFinishedSubscriber.php:
--------------------------------------------------------------------------------
1 | reporter()->printResult();
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/styles/blocks/console.scss:
--------------------------------------------------------------------------------
1 | .console {
2 | margin-bottom: var(--spacing-double);
3 |
4 | .Box-body {
5 | word-break: break-all;
6 | }
7 |
8 | &.console--ansi,
9 | &.console--snippet {
10 | .content {
11 | .ansi,
12 | .snippet {
13 | pre {
14 | background-color: var(--secondary-background-color);
15 | border: 0;
16 | }
17 | }
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/tests/fixtures/framework/jest/26/options.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "jest-1",
3 | "name": "Jest",
4 | "type": "jest",
5 | "command": "yarn test",
6 | "path": "",
7 | "runsInRemote": false,
8 | "remotePath": "",
9 | "sshHost": "",
10 | "sshUser": null,
11 | "sshPort": null,
12 | "sshIdentity": null,
13 | "active": false,
14 | "status": "idle",
15 | "proprietary": {},
16 | "sort": "name",
17 | "selected": 0,
18 | "canToggleTests": false
19 | }
20 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "Feature request"
3 | about: Request a feature that you think would improve Lode for everyone
4 |
5 | ---
6 |
7 | ### Describe the feature or problem you’d like to solve
8 |
9 | A clear and concise description of what the feature or problem is.
10 |
11 | ### Proposed solution
12 |
13 | How will it benefit Lode and its useΩrs?
14 |
15 | ### Additional context
16 |
17 | Add any other context like screenshots or mockups are helpful, if applicable.
18 |
--------------------------------------------------------------------------------
/tests/fixtures/process/4.json:
--------------------------------------------------------------------------------
1 | {
2 | "process": {
3 | "rawChunks": [
4 | "\n<<>>"
5 | ]
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/lib/reporters/phpunit-10/src/Subscriber/TestErrorTriggeredSubscriber.php:
--------------------------------------------------------------------------------
1 | reporter()->handleEvent($event);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/lib/reporters/phpunit-10/src/Subscriber/TestConsideredRiskySubscriber.php:
--------------------------------------------------------------------------------
1 | reporter()->handleEvent($event);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/lib/reporters/phpunit-10/src/Subscriber/TestNoticeTriggeredSubscriber.php:
--------------------------------------------------------------------------------
1 | reporter()->handleEvent($event);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/styles/blocks/breadcrumbs.scss:
--------------------------------------------------------------------------------
1 | @use "sass:color";
2 |
3 | .breadcrumbs {
4 | .breadcrumb-item {
5 | color: var(--color-fg-subtle);
6 | margin-left: 0;
7 |
8 | &::after {
9 | border: 0;
10 | color: var(--color-fg-subtle);
11 | opacity: .5;
12 | content: "●";
13 | }
14 | }
15 |
16 | ol {
17 | > :last-child {
18 | &::after {
19 | content: "";
20 | }
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/styles/blocks/draggable.scss:
--------------------------------------------------------------------------------
1 | // Dedicated draggable element to avoid
2 | // conflict with gutter dragging.
3 | .draggable {
4 | -webkit-app-region: drag;
5 | display: block;
6 | height: 100%;
7 | max-height: 43px;
8 | position: absolute;
9 | right: 0;
10 | top: 0;
11 | width: calc(100% - var(--pane-gutter-width));
12 |
13 | // Windows has no draggable helpers because
14 | // it has a title bar.
15 | @include win32 {
16 | display: none !important;
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/lib/reporters/phpunit-10/src/Subscriber/TestMarkedIncompleteSubscriber.php:
--------------------------------------------------------------------------------
1 | reporter()->handleEvent($event);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/lib/reporters/phpunit-10/src/Subscriber/TestWarningTriggeredSubscriber.php:
--------------------------------------------------------------------------------
1 | reporter()->handleEvent($event);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tests/fixtures/process/2.json:
--------------------------------------------------------------------------------
1 | {
2 | "process": {
3 | "rawChunks": [
4 | "\n<<>>"
6 | ]
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/tests/fixtures/process/3.json:
--------------------------------------------------------------------------------
1 | {
2 | "process": {
3 | "rawChunks": [
4 | "\n<<>>"
6 | ]
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/workflow/release-before.js:
--------------------------------------------------------------------------------
1 | const Path = require('path')
2 | const Fs = require('fs-extra')
3 | const builder = require('../electron-builder.js')
4 | const chalk = require('chalk')
5 |
6 | const releaseNotesPath = Path.join(__dirname, `../${builder.directories.buildResources}/release-notes.md`)
7 | if (!Fs.existsSync(releaseNotesPath)) {
8 | console.log(`\n${chalk.bgRed.white(' NO RELEASE NOTES ')} No release notes file found inside the buildResources directory. Please add it and try again.\n`)
9 | process.exit(1)
10 | }
11 |
--------------------------------------------------------------------------------
/src/lib/reporters/phpunit-10/src/Subscriber/TestPhpNoticeTriggeredSubscriber.php:
--------------------------------------------------------------------------------
1 | reporter()->handleEvent($event);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/lib/reporters/phpunit-10/src/Subscriber/TestPhpWarningTriggeredSubscriber.php:
--------------------------------------------------------------------------------
1 | reporter()->handleEvent($event);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tests/fixtures/framework/repositories.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "repository-1",
4 | "name": "hobnobs",
5 | "path": "/lodeapp/lode/hobnobs",
6 | "expanded": true
7 | },
8 | {
9 | "id": "repository-2",
10 | "name": "digestives",
11 | "path": "/lodeapp/lode/digestives",
12 | "expanded": false
13 | },
14 | {
15 | "id": "repository-3",
16 | "name": "rich-tea",
17 | "path": "/lodeapp/lode/rich-tea",
18 | "expanded": false
19 | }
20 | ]
21 |
--------------------------------------------------------------------------------
/src/lib/reporters/phpunit-10/src/Subscriber/TestDeprecationTriggeredSubscriber.php:
--------------------------------------------------------------------------------
1 | reporter()->handleEvent($event);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/main/index.dev.ts:
--------------------------------------------------------------------------------
1 | import { app } from 'electron'
2 |
3 | app.whenReady().then(() => {
4 | // @TODO: install Vue Devtools when they are compatible again with Electron 10
5 | // const installExtension = require('electron-devtools-installer')
6 | // installExtension(installExtension.VUEJS_DEVTOOLS)
7 | // .then(() => {})
8 | // .catch((err: Error) => {
9 | // console.log('Unable to install `vue-devtools`: \n', err)
10 | // })
11 | })
12 |
13 | // Require `main` process to boot app
14 | require('./index')
15 |
--------------------------------------------------------------------------------
/src/lib/reporters/phpunit-10/src/Subscriber/TestPhpunitErrorTriggeredSubscriber.php:
--------------------------------------------------------------------------------
1 | reporter()->handleEvent($event);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/lib/reporters/phpunit-10/src/Subscriber/TestSuiteStartedSubscriber.php:
--------------------------------------------------------------------------------
1 | reporter()->startTestSuite($event->testSuite())) {
15 | exit;
16 | }
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/tests/fixtures/process/1.json:
--------------------------------------------------------------------------------
1 | {
2 | "process": {
3 | "rawChunks": [
4 | "\n<<>>"
7 | ]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/tests/fixtures/process/9.json:
--------------------------------------------------------------------------------
1 | {
2 | "process": {
3 | "rawChunks": [
4 | "\n<<>>"
7 | ]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/tests/fixtures/process/21.json:
--------------------------------------------------------------------------------
1 | {
2 | "process": {
3 | "rawChunks": [
4 | "Starting...",
5 | "\n<<reporter()->handleEvent($event);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/src/lib/reporters/phpunit-10/src/Subscriber/TestPhpunitWarningTriggeredSubscriber.php:
--------------------------------------------------------------------------------
1 | reporter()->handleEvent($event);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tests/fixtures/framework/phpunit/8.0/options.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "phpunit-1",
3 | "name": "PHPUnit",
4 | "type": "phpunit",
5 | "command": "./vendor/bin/phpunit",
6 | "path": "",
7 | "runsInRemote": false,
8 | "remotePath": "",
9 | "sshHost": "",
10 | "sshUser": null,
11 | "sshPort": null,
12 | "sshIdentity": null,
13 | "active": false,
14 | "status": "idle",
15 | "proprietary": {
16 | "autoloadPath": ""
17 | },
18 | "sort": "framework",
19 | "selected": 0,
20 | "canToggleTests": true
21 | }
22 |
--------------------------------------------------------------------------------
/src/styles/blocks/test.scss:
--------------------------------------------------------------------------------
1 | .test {
2 | .test-name {
3 | height: 100%;
4 | left: calc(var(--status-block-width) + 9px);
5 | line-height: 2.5em;
6 | overflow: hidden;
7 | position: absolute;
8 | text-overflow: ellipsis;
9 | top: 0;
10 | white-space: nowrap;
11 |
12 | // 100% - left positioning - padding
13 | width: calc(100% - (var(--status-block-width) + 9px) - var(--status-block-width));
14 | }
15 |
16 | .selective-toggle + .test-name {
17 | margin-left: 24px;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/tests/fixtures/process/20.json:
--------------------------------------------------------------------------------
1 | {
2 | "process": {
3 | "rawChunks": [
4 | "Starting...",
5 | "\n<<>>"
7 | ]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/tests/fixtures/process/28.json:
--------------------------------------------------------------------------------
1 | {
2 | "process": {
3 | "rawChunks": [
4 | "\n<<>>hdkowhsftfg"
7 | ]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/lib/reporters/phpunit-10/src/Subscriber/Subscriber.php:
--------------------------------------------------------------------------------
1 | reporter;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/lib/reporters/phpunit-10/src/Subscriber/TestPhpunitDeprecationTriggeredSubscriber.php:
--------------------------------------------------------------------------------
1 | reporter()->handleEvent($event);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/tests/fixtures/process/16.json:
--------------------------------------------------------------------------------
1 | {
2 | "process": {
3 | "rawChunks": [
4 | "\n<<>>"
9 | ]
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/tests/fixtures/process/17.json:
--------------------------------------------------------------------------------
1 | {
2 | "process": {
3 | "rawChunks": [
4 | "\n<<>>"
9 | ]
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/src/styles/globals.scss:
--------------------------------------------------------------------------------
1 | html,
2 | body {
3 | background-color: var(--secondary-background-color);
4 | font-family: var(--font-family-sans-serif);
5 | font-size: 12px;
6 | height: 100%;
7 | margin: 0;
8 | overflow: hidden;
9 | padding: 0;
10 | user-select: none;
11 | width: 100%;
12 | }
13 |
14 | // stylelint-disable-next-line selector-max-id
15 | #app {
16 | display: flex;
17 | flex-direction: column;
18 | height: 100%;
19 | width: 100%;
20 | }
21 |
22 | a {
23 | color: var(--color-link);
24 | }
25 |
26 | * {
27 | cursor: default !important;
28 | }
29 |
--------------------------------------------------------------------------------
/tests/fixtures/process/19.json:
--------------------------------------------------------------------------------
1 | {
2 | "process": {
3 | "rawChunks": [
4 | "Starting...",
5 | "\n<<>>",
8 | "Ended!"
9 | ]
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/tests/fixtures/process/8.json:
--------------------------------------------------------------------------------
1 | {
2 | "process": {
3 | "rawChunks": [
4 | "\n<<>>"
10 | ]
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/tests/fixtures/process/25.json:
--------------------------------------------------------------------------------
1 | {
2 | "process": {
3 | "rawChunks": [
4 | "\n<<\n",
5 | "\n>>"
10 | ]
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/src/styles/blocks/terms.scss:
--------------------------------------------------------------------------------
1 | .terms {
2 | pre {
3 | background-color: transparent;
4 | border: 0;
5 | font-family: var(--font-family-sans-serif);
6 | padding: 0;
7 | white-space: normal;
8 |
9 | h1,
10 | h2,
11 | h3,
12 | h4,
13 | h5,
14 | h6 {
15 | margin-bottom: var(--spacing-half);
16 | }
17 |
18 | ol,
19 | ul {
20 | margin-left: var(--spacing-double);
21 |
22 | li {
23 | margin-bottom: var(--spacing-half);
24 | }
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/preload/lode.ts:
--------------------------------------------------------------------------------
1 | import { Ipc } from './ipc'
2 |
3 | class Preload {
4 | public readonly ipc: Ipc
5 | public readonly copyToClipboard: (string: string) => void
6 | public readonly openExternal: (link: string) => void
7 |
8 | constructor () {
9 | this.ipc = new Ipc()
10 |
11 | this.copyToClipboard = (string: string): void => {
12 | this.ipc.send('copy-to-clipboard', string)
13 | }
14 |
15 | this.openExternal = (link: string): void => {
16 | this.ipc.send('open-external-link', link)
17 | }
18 | }
19 | }
20 |
21 | export const Lode = new Preload()
22 |
--------------------------------------------------------------------------------
/tests/fixtures/process/6.json:
--------------------------------------------------------------------------------
1 | {
2 | "process": {
3 | "rawChunks": [
4 | "\n<<>>"
11 | ]
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/tests/fixtures/process/7.json:
--------------------------------------------------------------------------------
1 | {
2 | "process": {
3 | "rawChunks": [
4 | "\n<<>>"
11 | ]
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/renderer/plugins/alerts.ts:
--------------------------------------------------------------------------------
1 | import type { App, State } from 'vue'
2 | import type { Store } from 'vuex'
3 |
4 | export default class Alerts {
5 | private store: Store
6 |
7 | constructor (store: Store) {
8 | this.store = store
9 | }
10 |
11 | install (app: App) {
12 | app.config.globalProperties.$alert = this
13 | }
14 |
15 | show (alert: any) {
16 | this.store.dispatch('alert/show', alert)
17 | }
18 |
19 | hide () {
20 | this.store.dispatch('alert/hide')
21 | }
22 |
23 | clear () {
24 | this.store.dispatch('alert/clear')
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/tests/fixtures/process/18.json:
--------------------------------------------------------------------------------
1 | {
2 | "process": {
3 | "rawChunks": [
4 | "\n<<>>"
11 | ]
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/tests/fixtures/process/26.json:
--------------------------------------------------------------------------------
1 | {
2 | "process": {
3 | "rawChunks": [
4 | "\n<<>",
10 | "\n>"
11 | ]
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/renderer/components/modals/mixins/confirm.js:
--------------------------------------------------------------------------------
1 | import Modal from '@/components/modals/mixins/modal'
2 |
3 | export default {
4 | mixins: [
5 | Modal
6 | ],
7 | props: {
8 | resolve: {
9 | type: Function,
10 | required: true
11 | },
12 | reject: {
13 | type: Function,
14 | required: true
15 | }
16 | },
17 | methods: {
18 | confirm (data) {
19 | this.resolve(data)
20 | this.close()
21 | },
22 | cancel () {
23 | this.reject()
24 | this.close()
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/renderer/store/modules/ledger.js:
--------------------------------------------------------------------------------
1 | export default {
2 | namespaced: true,
3 | state: {
4 | ledger: {}
5 | },
6 | mutations: {
7 | SET (state, payload) {
8 | state.ledger = {}
9 | state.ledger = {
10 | ...state.ledger,
11 | ...payload
12 | }
13 | },
14 | UPDATE (state, payload) {
15 | state.ledger = {
16 | ...state.ledger,
17 | ...payload
18 | }
19 | }
20 | },
21 | getters: {
22 | ledger: state => {
23 | return state.ledger
24 | }
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/tests/fixtures/process/5.json:
--------------------------------------------------------------------------------
1 | {
2 | "process": {
3 | "rawChunks": [
4 | "\n<<>>"
12 | ]
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/styles/vendor.scss:
--------------------------------------------------------------------------------
1 | // ========================================================================
2 | // Vendor SCSS
3 | // ========================================================================
4 |
5 | @import "node_modules/@primer/css/color-modes/index";
6 | @import "node_modules/@primer/css/core/index";
7 | @import "node_modules/@primer/css/product/index";
8 |
9 | // ========================================================================
10 | // Vendor CSS
11 | // ========================================================================
12 |
13 | @import "node_modules/overlayscrollbars/css/OverlayScrollbars";
14 | @import "node_modules/@xterm/xterm/css/xterm"
15 |
--------------------------------------------------------------------------------
/src/lib/frameworks/index.ts:
--------------------------------------------------------------------------------
1 | import { find } from 'lodash'
2 | import { Jest } from './jest/framework'
3 | import { PHPUnit10 } from './phpunit-10/framework'
4 | import { PHPUnit } from './phpunit/framework'
5 |
6 | export { Jest }
7 | export { PHPUnit10 }
8 | export { PHPUnit }
9 |
10 | export const Frameworks = [Jest, PHPUnit10, PHPUnit]
11 |
12 | /**
13 | * Get a framework class by its type.
14 | *
15 | * @param type The slug representing the framework type.
16 | */
17 | export function getFrameworkByType (type: string): typeof Jest | typeof PHPUnit10 | typeof PHPUnit | undefined {
18 | return find(Frameworks, framework => framework.getDefaults().type === type)
19 | }
20 |
--------------------------------------------------------------------------------
/src/renderer/components/Split.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
26 |
--------------------------------------------------------------------------------
/src/styles/blocks/filename.scss:
--------------------------------------------------------------------------------
1 | .filename {
2 | @include clearfix;
3 |
4 | font-size: 0;
5 | user-select: text;
6 |
7 | > span {
8 | font-size: 1rem;
9 | }
10 |
11 | > .dir {
12 | color: var(--color-fg-subtler);
13 | word-break: break-all;
14 | }
15 |
16 | // Truncate variation has a different structure.
17 | &--truncate {
18 | color: var(--color-fg-subtler);
19 | font-size: 1rem;
20 | word-break: break-all;
21 |
22 | > strong {
23 | color: var(--color-fg-default);
24 | font-weight: var(--font-weight-normal);
25 | word-break: normal;
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/.github/workflows/codeql-analysis.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL"
2 |
3 | on:
4 | push:
5 | branches: [develop, main]
6 | pull_request:
7 | branches: [develop]
8 | schedule:
9 | - cron: '38 7 * * 5'
10 |
11 | jobs:
12 | analyze:
13 | name: Analyze
14 | runs-on: ubuntu-latest
15 |
16 | strategy:
17 | fail-fast: false
18 | matrix:
19 | language: ['javascript']
20 |
21 | steps:
22 | - uses: actions/checkout@v4
23 | - name: Initialize CodeQL
24 | uses: github/codeql-action/init@v3
25 | with:
26 | languages: ${{ matrix.language }}
27 | - name: Perform CodeQL Analysis
28 | uses: github/codeql-action/analyze@v3
29 |
--------------------------------------------------------------------------------
/src/renderer/store/modules/tabs.js:
--------------------------------------------------------------------------------
1 | export default {
2 | namespaced: true,
3 | state: {
4 | lastActive: ''
5 | },
6 | mutations: {
7 | SET_LAST_ACTIVE (state, payload) {
8 | state.lastActive = payload
9 | },
10 | CLEAR (state) {
11 | state.lastActive = ''
12 | }
13 | },
14 | actions: {
15 | setLastActive: ({ commit }, tab) => {
16 | commit('SET_LAST_ACTIVE', tab)
17 | },
18 | clear: ({ commit }) => {
19 | commit('CLEAR')
20 | }
21 | },
22 | getters: {
23 | lastActive: state => {
24 | return state.lastActive
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/styles/blocks/contents.scss:
--------------------------------------------------------------------------------
1 | .contents {
2 | display: flex;
3 | flex: 0 0 auto;
4 | flex-direction: row;
5 | height: 100%;
6 | position: relative;
7 |
8 | @include linux {
9 | border-top: 1px solid var(--color-sidebar-border);
10 | }
11 |
12 | .no-projects {
13 | flex: 1 1 auto;
14 | padding: 210px 0 0;
15 | text-align: center;
16 | width: 100%;
17 |
18 | @include darwin {
19 | -webkit-app-region: drag;
20 | }
21 |
22 | h1 {
23 | margin-bottom: var(--spacing-double);
24 | }
25 |
26 | > button {
27 | -webkit-app-region: no-drag;
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/tests/fixtures/process/23.json:
--------------------------------------------------------------------------------
1 | {
2 | "process": {
3 | "rawChunks": [
4 | "Starting...\r\n\r\n",
5 | "Still starting...\r\n\r\n",
6 | "\r\n",
7 | "<<>>\r\n",
11 | "Ended!\r\n"
12 | ]
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/lib/logger/main.ts:
--------------------------------------------------------------------------------
1 | import { log } from './index'
2 | import { formatLogMessage } from './format'
3 |
4 | const g = global as any
5 |
6 | g.log = {
7 | error (message: string | object, error?: Error) {
8 | log('error', '[main]: ' + formatLogMessage(message, error))
9 | },
10 | warn (message: string | object, error?: Error) {
11 | log('warn', '[main]: ' + formatLogMessage(message, error))
12 | },
13 | info (message: string | object, error?: Error) {
14 | log('info', '[main]: ' + formatLogMessage(message, error))
15 | },
16 | debug (message: string | object, error?: Error) {
17 | log('debug', '[main]: ' + formatLogMessage(message, error))
18 | }
19 | } as ILogger
20 |
--------------------------------------------------------------------------------
/tests/fixtures/process/29.json:
--------------------------------------------------------------------------------
1 | {
2 | "process": {
3 | "rawChunks": [
4 | "\n<<>>"
13 | ]
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/renderer/store/modules/status.js:
--------------------------------------------------------------------------------
1 | import { get } from 'lodash'
2 |
3 | export default {
4 | namespaced: true,
5 | state: {
6 | status: {}
7 | },
8 | mutations: {
9 | SET (state, payload) {
10 | state.status = {}
11 | state.status = {
12 | ...state.status,
13 | ...payload
14 | }
15 | },
16 | UPDATE (state, payload) {
17 | state.status = {
18 | ...state.status,
19 | ...payload
20 | }
21 | }
22 | },
23 | getters: {
24 | nugget: state => nugget => {
25 | return get(state.status, nugget, 'idle')
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/workflow/app-info.js:
--------------------------------------------------------------------------------
1 | const s = JSON.stringify
2 |
3 | module.exports.getReplacements = function () {
4 | return {
5 | __DARWIN__: process.platform === 'darwin',
6 | __WIN32__: process.platform === 'win32',
7 | __LINUX__: process.platform === 'linux',
8 | __DEV__: process.env.IS_DEV || false,
9 | __LOGGER__: process.env.LOGGER !== 'false',
10 | 'process.platform': s(process.platform),
11 | 'process.env.NODE_ENV': s(process.env.NODE_ENV || 'development'),
12 | 'process.env.TEST_ENV': s(process.env.TEST_ENV),
13 | __CRASH_URL__: s('https://71e593620d21420fb864d3fe667d8ce6@sentry.io/1476972'),
14 | __ANALYTICS_ID__: s('UA-103701546-4')
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/.stylelintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": [
3 | "stylelint-config-standard-scss",
4 | "stylelint-config-html/html",
5 | "stylelint-config-html/vue"
6 | ],
7 | "rules": {
8 | "comment-whitespace-inside": "always",
9 | "indentation": 4,
10 | "max-nesting-depth": [7, { "severity": "warning"}],
11 | "no-descending-specificity": null,
12 | "number-leading-zero": "never",
13 | "scss/comment-no-empty": null,
14 | "selector-class-pattern": null,
15 | "selector-max-compound-selectors": null,
16 | "selector-no-qualifying-type": null
17 | },
18 | "ignoreFiles": [
19 | "**/*.js",
20 | "**/*.ts",
21 | "**/*.json",
22 | "**/reporters/**",
23 | "src/styles/images/**"
24 | ]
25 | }
26 |
--------------------------------------------------------------------------------
/src/styles/blocks/about.scss:
--------------------------------------------------------------------------------
1 | .about {
2 | text-align: center;
3 |
4 | > img {
5 | height: 80px;
6 | width: 80px;
7 | }
8 |
9 | h4 {
10 | margin-bottom: var(--spacing);
11 | }
12 |
13 | .version {
14 | span {
15 | display: block;
16 | }
17 | }
18 |
19 | .legal {
20 | font-size: 0;
21 |
22 | a {
23 | border-right: 1px solid var(--color-border-muted);
24 | display: inline-block;
25 | font-size: 1rem;
26 | height: 14px;
27 | line-height: 14px;
28 | padding: 0 4px;
29 | }
30 |
31 | :last-child {
32 | border: 0;
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/renderer/components/MetaTable.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | {{ key }}
11 | {{ value }}
12 |
13 |
14 |
15 |
16 |
17 |
18 |
29 |
--------------------------------------------------------------------------------
/workflow/clean.js:
--------------------------------------------------------------------------------
1 | const Path = require('path')
2 | const Fs = require('fs-extra')
3 |
4 | const distPath = Path.join(__dirname, '../dist')
5 | const electronPath = Path.join(__dirname, '../dist')
6 | Fs.removeSync(Path.join(__dirname, '../static/reporters'))
7 | Fs.removeSync(Path.join(__dirname, '../support/icons'))
8 | Fs.removeSync(Path.join(__dirname, '../src/lib/reporters/phpunit/bootstrap'))
9 | Fs.removeSync(Path.join(__dirname, '../src/lib/reporters/phpunit-10/bootstrap'))
10 | Fs.removeSync(Path.join(__dirname, '../build/mac'))
11 | Fs.removeSync(Path.join(__dirname, '../build/win-unpacked'))
12 | Fs.removeSync(distPath)
13 | Fs.mkdirsSync(distPath)
14 | Fs.mkdirsSync(electronPath)
15 | Fs.closeSync(Fs.openSync(Path.join(electronPath, '.gitkeep'), 'w'))
16 |
--------------------------------------------------------------------------------
/src/index.ejs:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
10 |
14 | Lode
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/src/styles/blocks/feedback.scss:
--------------------------------------------------------------------------------
1 | .feedback {
2 | h4,
3 | .message {
4 | user-select: text;
5 | word-break: break-word;
6 | }
7 |
8 | .message {
9 | color: var(--color-fg-muted);
10 | font-size: 1.2rem;
11 | margin-bottom: var(--spacing-and-half);
12 | margin-top: var(--spacing-half);
13 |
14 | :not(:empty) {
15 | &:not(.ansi) {
16 | white-space: pre-wrap;
17 | }
18 | }
19 |
20 | code {
21 | background-color: var(--color-scale-blue-1);
22 | border-radius: 2px;
23 | color: var(--color-scale-blue-8);
24 | padding: 1px 5px;
25 | word-break: break-all;
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/styles/animations.scss:
--------------------------------------------------------------------------------
1 | // ========================================================================
2 | // Animations
3 | // ========================================================================
4 |
5 | .fade-enter-active,
6 | .fade-leave-active {
7 | transition: opacity .2s;
8 | }
9 |
10 | .fade-enter-from,
11 | .fade-leave-active {
12 | opacity: 0;
13 | }
14 |
15 | @keyframes rotate {
16 | 0% {
17 | transform: rotate(0deg);
18 | }
19 |
20 | 100% {
21 | transform: rotate(360deg);
22 | }
23 | }
24 |
25 | @keyframes fade-in-top {
26 | from {
27 | opacity: 0;
28 | transform: translateY(-15px);
29 | }
30 |
31 | to {
32 | opacity: 1;
33 | transform: translateX(0);
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/src/lib/reporters/phpunit-10/src/ExtensionHook.php:
--------------------------------------------------------------------------------
1 | configuration(),
20 | $facade,
21 | );
22 |
23 | $extensionBootstrapper->bootstrap(Extension::class, []);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/workflow/decipher.js:
--------------------------------------------------------------------------------
1 | const Fs = require('fs-extra')
2 | const Crypto = require('crypto')
3 |
4 | if (process.argv.length < 3) {
5 | throw Error('Missing project.db file path.')
6 | }
7 |
8 | const encryptionAlgorithm = 'aes-256-cbc'
9 | const encryptionKey = 'v1'
10 | const path = process.argv[2]
11 |
12 | // Decipher encrypted project.db files
13 | let data = Fs.readFileSync(path, null)
14 | const initializationVector = data.slice(0, 16)
15 | const password = Crypto.pbkdf2Sync(encryptionKey, initializationVector.toString(), 10000, 32, 'sha512')
16 | const decipher = Crypto.createDecipheriv(encryptionAlgorithm, password, initializationVector)
17 | data = Buffer.concat([decipher.update(data.slice(17)), decipher.final()])
18 |
19 | console.log(JSON.stringify(JSON.parse(data)))
20 |
--------------------------------------------------------------------------------
/src/renderer/store/modules/filters.js:
--------------------------------------------------------------------------------
1 | import { get, identity, isArray, isEmpty, pickBy } from 'lodash'
2 |
3 | export default {
4 | namespaced: true,
5 | state: {},
6 | mutations: {
7 | SET (state, { id, filters }) {
8 | // Set by merging current state and removing falsy or empty values
9 | state[id] = pickBy({
10 | ...get(state, id, {}),
11 | ...filters
12 | }, value => isArray(value) ? !isEmpty(value) : identity(value))
13 | },
14 | RESET (state) {
15 | Object.keys(state).forEach(id => {
16 | delete state[id]
17 | })
18 | }
19 | },
20 | getters: {
21 | all: state => id => {
22 | return state[id] || {}
23 | }
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/src/renderer/components/KeyValue.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{ key }}
5 |
6 |
{{ value }}
7 |
{{ value }}
8 |
9 |
10 |
11 |
12 |
13 |
31 |
--------------------------------------------------------------------------------
/tests/fixtures/process/13.json:
--------------------------------------------------------------------------------
1 | {
2 | "process": {
3 | "rawChunks": [
4 | "\n<<>>"
5 | ]
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/src/renderer/components/mixins/HasFrameworkMenu.js:
--------------------------------------------------------------------------------
1 | export default {
2 | data () {
3 | return {
4 | menuActive: false
5 | }
6 | },
7 | methods: {
8 | onContextMenu () {
9 | let rect
10 | if (this.$el.querySelector('.more-actions')) {
11 | rect = JSON.parse(JSON.stringify(this.$el.querySelector('.more-actions').getBoundingClientRect()))
12 | }
13 | this.menuActive = true
14 | Lode.ipc.invoke('framework-context-menu', this.model.id, rect)
15 | .finally(() => {
16 | this.menuActive = false
17 | const button = this.$el.querySelector('.more-actions')
18 | if (button) {
19 | button.blur()
20 | }
21 | })
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tests/fixtures/process/11.json:
--------------------------------------------------------------------------------
1 | {
2 | "process": {
3 | "rawChunks": [
4 | "\n<<>>"
6 | ]
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/tests/fixtures/process/12.json:
--------------------------------------------------------------------------------
1 | {
2 | "process": {
3 | "rawChunks": [
4 | "\n<<>>"
6 | ]
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/babel.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "comments": false,
3 | "env": {
4 | "main": {
5 | "presets": [
6 | ["@babel/preset-env", {
7 | "targets": { "node": 14 }
8 | }]
9 | ]
10 | },
11 | "renderer": {
12 | "presets": [
13 | ["@babel/preset-env", {
14 | "modules": false
15 | }]
16 | ]
17 | },
18 | "reporters": {
19 | "presets": [
20 | ["@babel/preset-env", {
21 | "modules": false
22 | }]
23 | ],
24 | "plugins": ["@babel/plugin-transform-modules-commonjs"]
25 | },
26 | "test": {
27 | "presets": [
28 | ["@babel/preset-env", {
29 | "modules": false
30 | }]
31 | ],
32 | "plugins": ["@babel/plugin-transform-modules-commonjs"]
33 | }
34 | },
35 | "plugins": ["lodash", "@babel/plugin-transform-runtime"]
36 | }
37 |
--------------------------------------------------------------------------------
/src/lib/frameworks/phpunit/suite.ts:
--------------------------------------------------------------------------------
1 | import { clipboard } from 'electron'
2 | import { Suite } from '@lib/frameworks/suite'
3 |
4 | export class PHPUnitSuite extends Suite {
5 | /**
6 | * Get this suite's class name.
7 | */
8 | public getClassName (): string {
9 | return this.getMeta('class', '').replace(/\\/g, '\\\\')
10 | }
11 |
12 | /**
13 | * Append items to a PHPUnit suite's context menu.
14 | */
15 | public contextMenu (): Array {
16 | return [{
17 | label: __DARWIN__
18 | ? 'Copy Class Name'
19 | : 'Copy class name',
20 | click: () => {
21 | clipboard.writeText(this.getClassName() || '')
22 | },
23 | enabled: !!this.getClassName(),
24 | before: ['copy-local']
25 | }]
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/lib/frameworks/phpunit-10/suite.ts:
--------------------------------------------------------------------------------
1 | import { clipboard } from 'electron'
2 | import { Suite } from '@lib/frameworks/suite'
3 |
4 | export class PHPUnit10Suite extends Suite {
5 | /**
6 | * Get this suite's class name.
7 | */
8 | public getClassName (): string {
9 | return this.getMeta('class', '').replace(/\\/g, '\\\\')
10 | }
11 |
12 | /**
13 | * Append items to a PHPUnit suite's context menu.
14 | */
15 | public contextMenu (): Array {
16 | return [{
17 | label: __DARWIN__
18 | ? 'Copy Class Name'
19 | : 'Copy class name',
20 | click: () => {
21 | clipboard.writeText(this.getClassName() || '')
22 | },
23 | enabled: !!this.getClassName(),
24 | before: ['copy-local']
25 | }]
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/tests/fixtures/process/10.json:
--------------------------------------------------------------------------------
1 | {
2 | "process": {
3 | "rawChunks": [
4 | "\n<<>>"
7 | ]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/tests/fixtures/process/14.json:
--------------------------------------------------------------------------------
1 | {
2 | "process": {
3 | "rawChunks": [
4 | "\n<<>>"
7 | ]
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/src/lib/frameworks/emitter.ts:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from 'events'
2 | import { ApplicationWindow } from '@main/application-window'
3 |
4 | /**
5 | * A special event emitter that can also emit events
6 | * to the renderer process from a given window.
7 | */
8 | export class ProjectEventEmitter extends EventEmitter {
9 | protected window: ApplicationWindow
10 |
11 | constructor (window: ApplicationWindow) {
12 | super()
13 | this.window = window
14 | }
15 |
16 | /**
17 | * Emit an event to the renderer process.
18 | */
19 | protected emitToRenderer (event: string, ...args: any[]): void {
20 | if (this.window.canReceiveEvents()) {
21 | this.window.send(event, args)
22 | }
23 | }
24 |
25 | /**
26 | * Get the emitter's application window.
27 | */
28 | public getApplicationWindow (): ApplicationWindow {
29 | return this.window
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/lib/process/queue.ts:
--------------------------------------------------------------------------------
1 | import Bottleneck from 'bottleneck'
2 | import { state } from '@lib/state'
3 |
4 | export interface IQueue {
5 | add (job: any): void
6 | stop (): void
7 | }
8 |
9 | class Queue implements IQueue {
10 | protected limiter: Bottleneck
11 |
12 | constructor () {
13 | this.limiter = new Bottleneck({
14 | maxConcurrent: state.get('concurrency')
15 | })
16 |
17 | // Listen for config changes on concurrency to update the limiter
18 | state.on('set:concurrency', (value: number) => {
19 | this.limiter.updateSettings({
20 | maxConcurrent: value
21 | })
22 | })
23 | }
24 |
25 | public add (job: any): void {
26 | const wrapped = this.limiter.wrap(job)
27 | wrapped()
28 | }
29 |
30 | public stop (): void {
31 | this.limiter.stop()
32 | }
33 | }
34 |
35 | export const queue = new Queue()
36 |
--------------------------------------------------------------------------------
/src/styles/blocks/meta.scss:
--------------------------------------------------------------------------------
1 | .meta {
2 | margin-bottom: var(--spacing-double);
3 |
4 | table {
5 | border-left: 1px solid var(--color-border-default);
6 | border-right: 1px solid var(--color-border-default);
7 | font-family: var(--font-family-monospace);
8 | font-size: 1rem;
9 | line-height: 1.5;
10 |
11 | .heading {
12 | font-weight: var(--font-weight-semibold);
13 | max-width: 250px;
14 | overflow: hidden;
15 | text-align: right;
16 | text-overflow: ellipsis;
17 | white-space: nowrap;
18 | }
19 |
20 | td {
21 | border-left: 0;
22 | padding: 3px 7px;
23 | user-select: text;
24 | }
25 |
26 | td:not(.heading) {
27 | border-right: 0;
28 | word-break: break-word;
29 | }
30 | }
31 | }
32 |
33 | .meta-group {
34 | padding-top: var(--spacing);
35 | }
36 |
--------------------------------------------------------------------------------
/tests/fixtures/process/15.json:
--------------------------------------------------------------------------------
1 | {
2 | "process": {
3 | "rawChunks": [
4 | "\n<<>>"
10 | ]
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 |
2 | {
3 | "version": "0.2.0",
4 | "configurations": [
5 | {
6 | "name": "Electron: Main",
7 | "type": "node",
8 | "request": "launch",
9 | "program": "${workspaceFolder}/src/main/index.dev.ts",
10 | "stopOnEntry": false,
11 | "args": [],
12 | "cwd": "${workspaceRoot}",
13 | "runtimeExecutable": "node",
14 | "runtimeArgs": [
15 | "${workspaceRoot}/workflow/run-dev.js"
16 | ],
17 | "env": {
18 | "IS_DEV": "true"
19 | },
20 | "sourceMaps": true
21 | },
22 | {
23 | "name": "Electron: Renderer",
24 | "type": "chrome",
25 | "request": "attach",
26 | "port": 9223,
27 | "webRoot": "${workspaceFolder}",
28 | "timeout": 30000
29 | }
30 | ],
31 | "compounds": [
32 | {
33 | "name": "Electron: All",
34 | "configurations": [
35 | "Electron: Main",
36 | "Electron: Renderer"
37 | ]
38 | }
39 | ]
40 | }
41 |
--------------------------------------------------------------------------------
/src/renderer/components/Indicator.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
35 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: "Bug report"
3 | about: Report a problem encountered while using Lode
4 |
5 | ---
6 |
7 | ### Describe the bug
8 |
9 | A clear and concise description of what the bug is.
10 |
11 | ### Version & OS
12 |
13 | Open 'About Lode' menu to see your version. Also include what operating system you are using.
14 |
15 | ### Steps to reproduce the behavior
16 |
17 | 1. Go to '...'
18 | 2. Click on '....'
19 | 3. Scroll down to '....'
20 | 4. See error
21 |
22 | ### Expected behavior
23 |
24 | A clear and concise description of what you expected to happen.
25 |
26 | ### Actual behavior
27 |
28 | A clear and concise description of what actually happened.
29 |
30 | ### Screenshots
31 |
32 | Add screenshots to help explain your problem, if applicable.
33 |
34 | ### Logs
35 |
36 | Attach your logs by opening the `Help` menu and selecting `Troubleshooting > Show Logs...`, if applicable.
37 |
38 | ### Additional context
39 |
40 | Add any other context about the problem here.
41 |
--------------------------------------------------------------------------------
/src/lib/frameworks/factory.ts:
--------------------------------------------------------------------------------
1 | import { ApplicationWindow } from '@main/application-window'
2 | import { FrameworkOptions, IFramework } from './framework'
3 | import { getFrameworkByType } from '@lib/frameworks'
4 |
5 | export class FrameworkFactory {
6 | /**
7 | * Make a new framework instance.
8 | *
9 | * @param window The application window which will own the framework
10 | * @param options The options to make the framework with
11 | */
12 | public static make (
13 | window: ApplicationWindow,
14 | options: FrameworkOptions
15 | ): IFramework {
16 | const Framework = getFrameworkByType(options.type)
17 |
18 | if (Framework) {
19 | // Create a new framework with hydrated options, in case defaults
20 | // ever change significantly from any persisted state.
21 | return new Framework(window, Framework.hydrate(options))
22 | }
23 |
24 | throw new Error(`Unknown framework type "${options.type}"`)
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/lib/logger/renderer.ts:
--------------------------------------------------------------------------------
1 | import { LogLevel } from './levels'
2 | import { formatLogMessage } from './format'
3 |
4 | const g = global as any
5 |
6 | function log (level: LogLevel, message: string | object, error?: Error) {
7 | Lode.ipc.send('log', level, '[renderer]: ' + formatLogMessage(message, error))
8 | }
9 |
10 | g.log = {
11 | error (message: string | object, error?: Error) {
12 | log('error', message, error)
13 | console.error(formatLogMessage(message, error))
14 | },
15 | warn (message: string | object, error?: Error) {
16 | log('warn', message, error)
17 | console.warn(formatLogMessage(message, error))
18 | },
19 | info (message: string | object, error?: Error) {
20 | log('info', message, error)
21 | console.info(formatLogMessage(message, error))
22 | },
23 | debug (message: string | object, error?: Error) {
24 | log('debug', message, error)
25 | console.debug(formatLogMessage(message, error))
26 | }
27 | } as ILogger
28 |
--------------------------------------------------------------------------------
/src/styles/blocks/trace.scss:
--------------------------------------------------------------------------------
1 | .trace {
2 | margin-bottom: var(--spacing-double);
3 | }
4 |
5 | .trace-group {
6 | .trace {
7 | margin-bottom: 0;
8 | }
9 |
10 | .trace-link {
11 | border-left: 2px solid var(--color-border-default);
12 | display: block;
13 | height: 35px;
14 | left: var(--spacing-double);
15 | position: relative;
16 |
17 | i {
18 | background-color: var(--color-canvas-default);
19 | border-radius: 22px;
20 | color: var(--color-fg-subtle);
21 | height: 20px;
22 | left: -11px;
23 | line-height: 21px;
24 | position: absolute;
25 | text-align: center;
26 | top: 7px;
27 | width: 20px;
28 | }
29 |
30 | span {
31 | color: var(--color-fg-muted);
32 | font-family: var(--font-family-monospace);
33 | left: 13px;
34 | position: relative;
35 | top: 7px;
36 | }
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "ES2020",
4 | "moduleResolution": "node",
5 | "esModuleInterop": true,
6 | "allowSyntheticDefaultImports": true,
7 | "target": "ES2020",
8 | "noImplicitReturns": true,
9 | "noFallthroughCasesInSwitch": true,
10 | "noUnusedLocals": true,
11 | "strict": true,
12 | "sourceMap": true,
13 | "jsx": "preserve",
14 | "outDir": "./dist",
15 | "types" : [ "node", "lodash" ],
16 | "typeRoots": [
17 | "./node_modules/@types",
18 | "./src/types"
19 | ],
20 | "baseUrl": "./src",
21 | "paths": {
22 | "@/*": ["./renderer/*"],
23 | "@main/*": ["./main/*"],
24 | "@lib/*": ["./lib/*"]
25 | },
26 | "allowJs": true,
27 | "lib": [
28 | "DOM",
29 | "ES2020"
30 | ]
31 | },
32 | "exclude": [
33 | "node_modules",
34 | "build"
35 | ],
36 | "include": [
37 | "src/**/*.ts"
38 | ],
39 | "compileOnSave": false
40 | }
41 |
--------------------------------------------------------------------------------
/src/lib/logger/preload.ts:
--------------------------------------------------------------------------------
1 | import { ipcRenderer } from 'electron'
2 | import { LogLevel } from './levels'
3 | import { formatLogMessage } from './format'
4 |
5 | const g = global as any
6 |
7 | function log (level: LogLevel, message: string | object, error?: Error) {
8 | ipcRenderer.send('log', level, '[preload]: ' + formatLogMessage(message, error))
9 | }
10 |
11 | g.log = {
12 | error (message: string | object, error?: Error) {
13 | log('error', message, error)
14 | console.error(formatLogMessage(message, error))
15 | },
16 | warn (message: string | object, error?: Error) {
17 | log('warn', message, error)
18 | console.warn(formatLogMessage(message, error))
19 | },
20 | info (message: string | object, error?: Error) {
21 | log('info', message, error)
22 | console.info(formatLogMessage(message, error))
23 | },
24 | debug (message: string | object, error?: Error) {
25 | log('debug', message, error)
26 | console.debug(formatLogMessage(message, error))
27 | }
28 | } as ILogger
29 |
--------------------------------------------------------------------------------
/src/renderer/store/modules/alert.js:
--------------------------------------------------------------------------------
1 | import app from '@'
2 |
3 | export default {
4 | namespaced: true,
5 | state: {
6 | alerts: []
7 | },
8 | mutations: {
9 | ADD (state, payload) {
10 | state.alerts.push(payload)
11 | },
12 | REMOVE (state) {
13 | state.alerts.pop()
14 | },
15 | CLEAR (state) {
16 | state.alerts = []
17 | }
18 | },
19 | actions: {
20 | show: ({ state, commit }, payload) => {
21 | commit('ADD', payload)
22 | if (state.alerts.length === 1) {
23 | app.config.globalProperties.$modal.open('AlertStack', {}, () => {
24 | commit('CLEAR')
25 | })
26 | }
27 | },
28 | hide: ({ commit }) => {
29 | commit('REMOVE')
30 | },
31 | clear: ({ commit }) => {
32 | commit('CLEAR')
33 | }
34 | },
35 | getters: {
36 | alerts: state => {
37 | return state.alerts
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/lib/themes/index.ts:
--------------------------------------------------------------------------------
1 | import { nativeTheme } from 'electron'
2 | import {
3 | isMacOSMojaveOrLater,
4 | isWindows10And1809Preview17666OrLater
5 | } from '@lib/helpers/os'
6 |
7 | /**
8 | * The list of available theme names.
9 | */
10 | export type ThemeName = 'light' | 'dark' | 'system'
11 |
12 | /**
13 | * Whether or not the current OS supports System Theme Changes
14 | */
15 | export function supportsSystemThemeChanges (): boolean {
16 | if (__DARWIN__) {
17 | return isMacOSMojaveOrLater()
18 | } else if (__WIN32__) {
19 | // Its technically possible this would still work on prior versions of Windows 10 but 1809
20 | // was released October 2nd, 2018 and the feature can just be "attained" by upgrading
21 | // See https://github.com/desktop/desktop/issues/9015 for more
22 | return isWindows10And1809Preview17666OrLater()
23 | }
24 |
25 | return false
26 | }
27 |
28 | export function initializeTheme (theme: ThemeName): void {
29 | if (theme !== 'system') {
30 | nativeTheme.themeSource = theme
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/renderer/components/modals/RemoveProject.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Remove Project
6 |
7 |
8 |
{{ `Are you sure you want to remove project **:0** from Lode? This cannot be undone.` }}
9 |
10 |
11 |
19 |
20 |
21 |
22 |
23 |
31 |
--------------------------------------------------------------------------------
/tests/cypress/support/ipc.js:
--------------------------------------------------------------------------------
1 | import electron from '../../mocks/electron'
2 |
3 | Cypress.Commands.add('ipcEvent', (...args) => {
4 | // Allow functions that resolve into arguments
5 | if (args.length === 1 && typeof args[0] === 'function') {
6 | args = args[0]()
7 | }
8 |
9 | cy.then(() => {
10 | electron.ipcRenderer.trigger(...args)
11 | })
12 | })
13 |
14 | Cypress.Commands.add('ipcResetMockHistory', () => {
15 | cy.then(() => {
16 | return new Promise((resolve) => {
17 | if (electron.ipcRenderer.send.resetHistory) {
18 | electron.ipcRenderer.send.resetHistory()
19 | }
20 | if (electron.ipcRenderer.invoke.resetHistory) {
21 | electron.ipcRenderer.invoke.resetHistory()
22 | }
23 | resolve()
24 | })
25 | })
26 | })
27 |
28 | Cypress.Commands.add('ipcInvocation', index => {
29 | cy.wrap(electron.ipcRenderer.invoke.getCall(index))
30 | })
31 |
32 | Cypress.Commands.add('ipcEmission', index => {
33 | cy.wrap(electron.ipcRenderer.send.getCall(index))
34 | })
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) Tomas Buteler
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/src/renderer/store/modules/theme.js:
--------------------------------------------------------------------------------
1 | import { OneHalfDark, OneHalfLight } from 'xterm-theme'
2 |
3 | // xterm.js will process colors loaded as themes, so
4 | // we need to give it hex colors as strings rather
5 | // than CSS variables. So we'll set up the terminal
6 | // theme as an application state, mutating it when
7 | // theme changes from main process.
8 | const getColors = theme => {
9 | if (theme === 'dark') {
10 | return {
11 | ...OneHalfDark,
12 | // Must match var(--secondary-background-color)
13 | background: '#22272e'
14 | }
15 | }
16 |
17 | return {
18 | ...OneHalfLight,
19 | // Must match var(--secondary-background-color)
20 | background: '#f6f8fa'
21 | }
22 | }
23 |
24 | export default {
25 | namespaced: true,
26 | state: {
27 | colors: {}
28 | },
29 | mutations: {
30 | SET (state, theme) {
31 | state.colors = getColors(theme)
32 | }
33 | },
34 | getters: {
35 | colors: state => {
36 | return state.colors
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/styles/blocks/code.scss:
--------------------------------------------------------------------------------
1 | .ansi,
2 | .snippet {
3 | position: relative;
4 |
5 | button {
6 | appearance: none;
7 | border-radius: 3px;
8 | border: 1px solid var(--primary-border-color);
9 | float: right;
10 | height: 30px;
11 | margin-left: 4px;
12 | opacity: 0;
13 | outline: none;
14 | padding-left: 0;
15 | padding-right: 0;
16 | position: absolute;
17 | right: 4px;
18 | top: 4px;
19 | transition: opacity 100ms ease-in;
20 | width: 35px;
21 | z-index: 1;
22 |
23 | &:nth-of-type(2) {
24 | right: 42px;
25 | }
26 |
27 | i {
28 | color: var(--color-fg-subtler);
29 | position: relative;
30 | top: -1px;
31 | }
32 |
33 | &:hover {
34 | i {
35 | color: var(--color-link);
36 | }
37 | }
38 | }
39 |
40 | &:not(.is-loading):hover {
41 | button {
42 | opacity: 1;
43 | transition-delay: 200ms;
44 | }
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/src/renderer/components/modals/ResetSettings.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Reset Settings
6 |
7 |
8 |
Are you sure you want to erase your saved settings? You will have to add all projects and repositories again.
9 |
This cannot be undone.
10 |
11 |
12 |
20 |
21 |
22 |
23 |
24 |
32 |
--------------------------------------------------------------------------------
/src/styles/blocks/ansi.scss:
--------------------------------------------------------------------------------
1 | .ansi {
2 | &.is-loading {
3 | overflow: hidden;
4 | }
5 |
6 | .parsed {
7 | .xterm-viewport {
8 | display: none;
9 | }
10 |
11 | .xterm-screen {
12 | height: auto !important;
13 | width: 100% !important;
14 | }
15 |
16 | .xterm-rows {
17 | background-color: transparent;
18 | user-select: text;
19 | width: 100% !important;
20 |
21 | > div {
22 | // Force line wrapping of terminal content
23 | display: inline-block;
24 | width: 100% !important;
25 | height: auto !important;
26 | word-break: break-all;
27 |
28 | span {
29 | width: auto !important;
30 | }
31 | }
32 | }
33 |
34 | .xterm-cursor {
35 | display: none;
36 | }
37 | }
38 |
39 | .terminal-mount {
40 | height: 0 !important;
41 | visibility: hidden;
42 | width: 0 !important;
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/tests/cypress/e2e/theme.js:
--------------------------------------------------------------------------------
1 | describe('Themes', () => {
2 | it('can use light theme on load and switch when notified', () => {
3 | cy
4 | .start()
5 | .get('html')
6 | .invoke('attr', 'data-color-mode')
7 | .should('equal', 'light')
8 | .get('html')
9 | .invoke('attr', 'data-light-theme')
10 | .should('equal', 'light')
11 | .get('html')
12 | .invoke('attr', 'data-dark-theme')
13 | .should('equal', 'dark_dimmed')
14 | .ipcEvent('theme-updated', 'dark')
15 | .get('html')
16 | .invoke('attr', 'data-color-mode')
17 | .should('equal', 'dark')
18 | .ipcEvent('theme-updated', 'light')
19 | .get('html')
20 | .invoke('attr', 'data-color-mode')
21 | .should('equal', 'light')
22 | })
23 |
24 | it('can use dark theme on load', () => {
25 | cy
26 | .start({ theme: 'dark' })
27 | .get('html')
28 | .invoke('attr', 'data-color-mode')
29 | .should('equal', 'dark')
30 | })
31 | })
32 |
--------------------------------------------------------------------------------
/src/renderer/components/modals/Terms.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
16 |
17 |
18 |
19 |
20 |
41 |
--------------------------------------------------------------------------------
/tests/lib/helpers/durations.spec.js:
--------------------------------------------------------------------------------
1 | import Durations from '@lib/helpers/durations'
2 |
3 | const helper = new Durations()
4 |
5 | it('formats durations', () => {
6 | expect(helper.format(0)).toBe('0ms')
7 | expect(helper.format(1)).toBe('1ms')
8 | expect(helper.format(166)).toBe('166ms')
9 | expect(helper.format(3660)).toBe('3.66s')
10 | expect(helper.format(300000)).toBe('5 min')
11 | expect(helper.format(300003)).toBe('5 min 3ms')
12 | expect(helper.format(303303)).toBe('5 min 3s')
13 | expect(helper.format(34500000)).toBe('9 hours 35 min')
14 | expect(helper.format(34500003)).toBe('9 hours 35 min')
15 | expect(helper.format(34503303)).toBe('9 hours 35 min 3s')
16 | expect(helper.format(134500000)).toBe('1 day 13 hours 21 min 40s')
17 | expect(helper.format(134500003)).toBe('1 day 13 hours 21 min 40s')
18 | expect(helper.format(134503303)).toBe('1 day 13 hours 21 min 43s')
19 | expect(helper.format(2134503303)).toBe('24 days 16 hours 55 min 3s')
20 | expect(helper.format(52134503303)).toBe('603 days 9 hours 48 min 23s')
21 | expect(helper.format(995213400000)).toBe('11518 days 16 hours 10 min')
22 | })
23 |
--------------------------------------------------------------------------------
/src/renderer/components/modals/ProjectLoadingFailed.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Project Failed to Load
6 |
7 |
8 |
The data for the project might've been deleted or corrupted, and will need to be removed.
9 |
10 |
11 |
19 |
20 |
21 |
22 |
23 |
31 |
--------------------------------------------------------------------------------
/src/lib/process/runners/yarn.ts:
--------------------------------------------------------------------------------
1 | import { concat } from 'lodash'
2 | import { IProcess, DefaultProcess } from '@lib/process/process'
3 |
4 | export class YarnProcess extends DefaultProcess implements IProcess {
5 | static readonly type: string = 'yarn'
6 |
7 | /**
8 | * Whether this process owns a given command.
9 | *
10 | * @param command The command we're checking to match a Yarn runner.
11 | */
12 | public static owns (command: string): boolean {
13 | return command.toLowerCase().search(/\byarn(\.js|\.cmd)?(?!\.)\b/) > -1
14 | }
15 |
16 | /**
17 | * Return the array of arguments with which to spawn the child process.
18 | * We need to patch the Yarn binary path for Windows environments.
19 | */
20 | protected spawnArguments (args: Array): Array {
21 | if (!args.length) {
22 | return args
23 | }
24 |
25 | let binary = args.shift()
26 | if (this.platform === 'win32' && binary === 'yarn') {
27 | binary = 'yarn.cmd'
28 | }
29 |
30 | // Recreate the arguments by prefixing remaining ones with '--'.
31 | return concat(binary!, args)
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/lib/process/shell.ts:
--------------------------------------------------------------------------------
1 | import * as shellEnv from 'shell-env'
2 | import * as defaultShell from 'default-shell'
3 |
4 | /**
5 | * The names of any env vars that we shouldn't copy from the shell environment.
6 | */
7 | const BlacklistedNames = new Set(['LOCAL_GIT_DIRECTORY'])
8 |
9 | /**
10 | * Merge environment variables from shell into the current process, if needed.
11 | */
12 | export function mergeEnvFromShell (): void {
13 | if (!needsEnv(process)) {
14 | return
15 | }
16 |
17 | const env = shellEnv.sync(getUserShell())
18 | for (const key in env) {
19 | if (BlacklistedNames.has(key)) {
20 | continue
21 | }
22 |
23 | process.env[key] = env[key]
24 | }
25 | }
26 |
27 | /**
28 | * Whether the current process needs to have shell environment
29 | * variables merged in.
30 | *
31 | * @param process The process to inspect.
32 | */
33 | function needsEnv (process: NodeJS.Process): boolean {
34 | return __DARWIN__ && !process.env.PWD
35 | }
36 |
37 | /**
38 | * Get the user-defined shell, if any.
39 | */
40 | function getUserShell () {
41 | if (process.env.SHELL) {
42 | return process.env.SHELL
43 | }
44 |
45 | return defaultShell
46 | }
47 |
--------------------------------------------------------------------------------
/src/renderer/components/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Welcome to Lode.
7 | Add your first project
8 |
9 |
12 |
17 |
18 |
19 |
20 |
21 |
22 |
38 |
--------------------------------------------------------------------------------
/src/renderer/components/modals/RemoveFramework.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Remove Framework
6 |
7 |
8 |
{{ 'Are you sure you want to remove **:0** from this project? This will not delete any tests from your filesystem and cannot be undone.' }}
9 |
10 |
11 |
19 |
20 |
21 |
22 |
23 |
37 |
--------------------------------------------------------------------------------
/src/styles/blocks/loading.scss:
--------------------------------------------------------------------------------
1 | .loading {
2 | align-items: center;
3 | display: flex;
4 | flex-direction: column;
5 | flex-grow: 1;
6 | padding-top: 15%;
7 |
8 | > .loading-group {
9 | animation: fade-in-top .4s forwards;
10 | animation-delay: .2s; // Reduce flicker when loading quickly.
11 | opacity: 0;
12 |
13 | .spinner,
14 | .spinner::after {
15 | border-radius: 50%;
16 | height: 30em;
17 | width: 30em;
18 | }
19 |
20 | .spinner {
21 | animation: rotate .6s infinite linear;
22 | border-bottom: 1em solid var(--color-accent-muted);
23 | border-left: 1em solid var(--color-accent-emphasis);
24 | border-right: 1em solid var(--color-accent-muted);
25 | border-top: 1em solid var(--color-accent-muted);
26 | display: block;
27 | font-size: 2px;
28 | left: 0;
29 | margin: 0 auto var(--spacing);
30 | position: relative;
31 | text-indent: -9999em;
32 | transform: translateZ(0);
33 | }
34 |
35 | h2 {
36 | color: var(--color-fg-subtle);
37 | font-weight: var(--font-weight-semibold);
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/renderer/components/mixins/HasFile.js:
--------------------------------------------------------------------------------
1 | import * as Path from 'path'
2 | import { mapGetters } from 'vuex'
3 | import Filename from '@/components/Filename.vue'
4 |
5 | export default {
6 | components: {
7 | Filename
8 | },
9 | data () {
10 | return {
11 | activeContextMenu: null
12 | }
13 | },
14 | computed: {
15 | ...mapGetters({
16 | rootPath: 'context/rootPath',
17 | repositoryPath: 'context/repositoryPath'
18 | })
19 | },
20 | methods: {
21 | relativePath (path) {
22 | if (!this.rootPath || !path.startsWith('/')) {
23 | return path
24 | }
25 |
26 | return Path.relative(this.rootPath, path)
27 | },
28 | absoluteLocalPath (file) {
29 | return Path.join(this.repositoryPath, this.relativePath(file))
30 | },
31 | onContextMenu (file, index) {
32 | this.activeContextMenu = index
33 | Lode.ipc.invoke('file-context-menu', this.absoluteLocalPath(file)).finally(() => {
34 | this.activeContextMenu = null
35 | })
36 | },
37 | hasContextMenu (index) {
38 | return this.activeContextMenu === index
39 | }
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/styles/blocks/results.scss:
--------------------------------------------------------------------------------
1 | .results {
2 | background-color: var(--primary-background-color);
3 | flex-grow: 1;
4 |
5 | &.blankslate {
6 | background-color: var(--secondary-background-color);
7 | border: 0;
8 | border-radius: 0;
9 | box-shadow: none;
10 | color: var(--color-fg-subtle);
11 | opacity: .4;
12 | }
13 |
14 | > .has-status {
15 | display: flex;
16 | flex-direction: column;
17 | height: calc(100vh - var(--titlebar-height));
18 |
19 | > .header {
20 | background-color: var(--secondary-background-color);
21 | padding-bottom: 15px;
22 |
23 | h2 {
24 | -webkit-app-region: no-drag;
25 | flex-grow: 0 !important;
26 | flex-shrink: 1 !important;
27 | font-size: 1.6rem;
28 | line-height: 1.9rem;
29 | user-select: text;
30 | }
31 |
32 | .breadcrumbs {
33 | padding: 0 15px;
34 |
35 | li {
36 | display: inline;
37 | user-select: text;
38 | white-space: normal;
39 | word-break: break-all;
40 | }
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/tests/cypress/support/process.js:
--------------------------------------------------------------------------------
1 | import { Lode } from '@preload/lode'
2 | import electron from '../../mocks/electron'
3 |
4 | Cypress.Commands.add('start', (options = {}) => {
5 | cy
6 | .visit('/', {
7 | onBeforeLoad (win) {
8 | win.Lode = Lode
9 | },
10 | onLoad (win) {
11 | cy.spy(electron.ipcRenderer, 'send')
12 | electron.ipcRenderer.trigger('did-finish-load', {
13 | ...{
14 | theme: 'light',
15 | version: '0.0.0',
16 | focus: true
17 | },
18 | ...options
19 | })
20 | }
21 | })
22 | .nextTick()
23 | })
24 |
25 | Cypress.Commands.add('startWithProject', (options = {}) => {
26 | cy
27 | .start(options)
28 | .fixture('framework/project.json')
29 | .then(project => {
30 | electron.ipcRenderer.trigger('project-ready', project)
31 | })
32 | .wait(1)
33 | })
34 |
35 | Cypress.Commands.add('nextTick', callback => {
36 | if (callback) {
37 | cy
38 | .wait(1)
39 | .then(callback)
40 | .wait(1)
41 | } else {
42 | cy
43 | .wait(1)
44 | }
45 | })
46 |
--------------------------------------------------------------------------------
/src/renderer/store/modules/expand.js:
--------------------------------------------------------------------------------
1 | export default {
2 | namespaced: true,
3 | state: {},
4 | mutations: {
5 | TOGGLE (state, identifier) {
6 | if (!state[identifier]) {
7 | state[identifier] = true
8 | return
9 | }
10 | delete state[identifier]
11 | },
12 | EXPAND (state, identifier) {
13 | state[identifier] = true
14 | },
15 | COLLAPSE (state, identifier) {
16 | delete state[identifier]
17 | },
18 | COLLAPSE_ALL (state) {
19 | for (const identifier in state) {
20 | delete state[identifier]
21 | }
22 | }
23 | },
24 | actions: {
25 | toggle: ({ state, commit }, identifier) => {
26 | commit('TOGGLE', identifier)
27 | },
28 | expand: ({ state, commit }, identifier) => {
29 | commit('EXPAND', identifier)
30 | },
31 | collapse: ({ state, commit }, identifier) => {
32 | commit('COLLAPSE', identifier)
33 | },
34 | collapseAll: ({ state, commit }) => {
35 | commit('COLLAPSE_ALL')
36 | }
37 | },
38 | getters: {
39 | expanded: state => id => {
40 | return !!state[id]
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/renderer/components/ModalController.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
12 |
13 |
14 |
15 |
16 |
17 |
45 |
--------------------------------------------------------------------------------
/src/lib/state/project.ts:
--------------------------------------------------------------------------------
1 | import ElectronStore from 'electron-store'
2 | import { ProjectOptions } from '@lib/frameworks/project'
3 |
4 | export class Project {
5 | protected store: any
6 |
7 | constructor (id: string) {
8 | log.info('Initializing project store: ' + id)
9 | this.store = new ElectronStore({
10 | encryptionKey: 'v1',
11 | name: 'project',
12 | defaults: {
13 | options: {
14 | id
15 | }
16 | },
17 | fileExtension: 'db',
18 | cwd: `Projects/${id}`
19 | })
20 | }
21 |
22 | public getPath (): string {
23 | return this.store.path.replace(/\/project\.db$/i, '')
24 | }
25 |
26 | public get (key?: string, fallback?: any): any {
27 | if (!key) {
28 | return this.store.store
29 | }
30 |
31 | return this.store.get(key, fallback)
32 | }
33 |
34 | public set (key: string, value?: any): void {
35 | this.store.set(key, value)
36 | }
37 |
38 | public save (options: ProjectOptions): void {
39 | this.store.set('options', {
40 | ...this.store.get('options'),
41 | ...options
42 | })
43 | }
44 |
45 | public toJSON (): ProjectOptions {
46 | return this.store.get('options')
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/tests/renderer/components/TestInformation.spec.js:
--------------------------------------------------------------------------------
1 | import { config, shallowMount } from '@vue/test-utils'
2 | import TestInformation from '@/components/TestInformation'
3 | import Strings from '@/plugins/strings'
4 |
5 | config.global.plugins = [new Strings()]
6 |
7 | const RealDate = Date.now
8 | beforeAll(() => {
9 | global.Date.now = jest.fn(() => new Date('2020-12-01T14:49:00').getTime())
10 | })
11 | afterAll(() => {
12 | global.Date.now = RealDate
13 | })
14 |
15 | test.each([
16 | ['never run', {
17 | first: '1985-10-21T09:00:00'
18 | }],
19 | ['dates only', {
20 | first: '1985-10-21T09:00:00',
21 | last: '2015-10-21T13:00:00'
22 | }],
23 | ['with duration', {
24 | first: '2020-09-10T13:00:00',
25 | last: '2020-11-21T13:00:00',
26 | duration: 1000
27 | }],
28 | ['with long duration', {
29 | first: '2020-12-01T10:20:00',
30 | last: '2020-12-01T14:47:00',
31 | duration: 32198
32 | }],
33 | ['with assertions', {
34 | first: '2020-11-21T13:00:00',
35 | last: '2020-12-01T14:48:33',
36 | assertions: 42
37 | }]
38 | ])('matches snapshot for stats: "%s"', async (name, stats) => {
39 | const wrapper = shallowMount(TestInformation, {
40 | propsData: {
41 | stats
42 | }
43 | })
44 | expect(wrapper.html()).toMatchSnapshot()
45 | })
46 |
--------------------------------------------------------------------------------
/tests/lib/frameworks/factory.spec.js:
--------------------------------------------------------------------------------
1 | import { ApplicationWindow } from '@main/application-window'
2 | import { FrameworkFactory } from '@lib/frameworks/factory'
3 | import { PHPUnit } from '@lib/frameworks/phpunit/framework'
4 |
5 | jest.mock('@lib/state')
6 | jest.mock('electron-store')
7 | jest.mock('@main/application-window')
8 |
9 | it('can make a new framework', async () => {
10 | const window = new ApplicationWindow()
11 | const options = {
12 | id: '42',
13 | type: 'phpunit'
14 | }
15 |
16 | const framework = FrameworkFactory.make(window, options)
17 | expect(framework).toBeInstanceOf(PHPUnit)
18 | // Persists id
19 | expect(framework.id).toBe('42')
20 | // Hydrates with default options, including proprietary and OS-specific
21 | expect(framework.name).toBe('PHPUnit (Legacy)')
22 | expect(framework.proprietary).toEqual({
23 | autoloadPath: ''
24 | })
25 | expect(framework.command).toBe(
26 | __WIN32__
27 | ? 'php vendor/phpunit/phpunit/phpunit'
28 | : './vendor/bin/phpunit'
29 | )
30 | })
31 |
32 | it('fails if type does not exist', async () => {
33 | const window = new ApplicationWindow()
34 | const options = {
35 | type: 'biscuit'
36 | }
37 |
38 | expect(() => {
39 | FrameworkFactory.make(window, options)
40 | }).toThrow('Unknown framework type "biscuit"')
41 | })
42 |
--------------------------------------------------------------------------------
/static/icons/gem.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | 'globalSetup': './tests/setup.js',
3 | 'globals': {
4 | __DEV__: true,
5 | __SILENT__: true,
6 | __WIN32__: process.platform === 'win32',
7 | __DARWIN__: process.platform === 'darwin',
8 | __LINUX__: process.platform === 'linux',
9 | 'ts-jest': {
10 | 'babelConfig': true,
11 | 'tsconfig': 'tsconfig.json'
12 | }
13 | },
14 | 'setupFiles': [
15 | './tests/mocks/setup.js'
16 | ],
17 | 'moduleFileExtensions': [
18 | 'js',
19 | 'ts',
20 | 'json',
21 | 'vue'
22 | ],
23 | 'moduleNameMapper': {
24 | '^@/(.*)$': '/src/renderer/$1',
25 | '^@main/(.*)$': '/src/main/$1',
26 | '^@lib/(.*)$': '/src/lib/$1'
27 | },
28 | 'testPathIgnorePatterns':
29 | [
30 | '/build/',
31 | '/dist/',
32 | '/node_modules/',
33 | '/src/'
34 | ],
35 | 'transform': {
36 | '.*\\.(vue)$': 'vue-jest',
37 | '^.+\\.(ts)$': 'ts-jest',
38 | '^.+\\.js$': 'babel-jest'
39 | },
40 | 'clearMocks': true,
41 | 'collectCoverage': false,
42 | 'collectCoverageFrom': [
43 | 'src/**',
44 | '!src/styles/**'
45 | ],
46 | 'coverageDirectory': 'tests/coverage',
47 | 'coverageReporters': ['json', 'html']
48 | }
49 |
--------------------------------------------------------------------------------
/src/lib/helpers/paths.ts:
--------------------------------------------------------------------------------
1 | import * as Path from 'path'
2 |
3 | export function getResourceDirectory (): string {
4 | return __DEV__
5 | ? Path.join(process.cwd(), 'dist')
6 | : Path.join(process.resourcesPath, 'app.asar.unpacked', 'dist')
7 | }
8 |
9 | /**
10 | * Modifies a given path to return its location as an unpacked asset
11 | * (i.e. not part of the app's package, so publicly available. This is
12 | * useful for reporters, which have to be read or injected by the
13 | * filesystem during test framework runs.)
14 | *
15 | * @param loc The path to process.
16 | */
17 | export function unpacked (loc: string): string {
18 | const s = Path.sep
19 | return loc.replace(/[\\\/]?\bapp\.asar\b[\\\/]?/, `${s}app.asar.unpacked${s}`)
20 | }
21 |
22 | /**
23 | * Rebuild a POSIX path into a platform-specific one, by replacing
24 | * its path separators. This is because we hardcode all our paths
25 | * as POSIX for readability.
26 | *
27 | * @param loc The path to process.
28 | */
29 | export function loc (loc: string): string {
30 | return loc.split('/').join(Path.sep)
31 | }
32 |
33 | /**
34 | * Force a path (potentially Windows-specific) into a POSIX one. Not
35 | * necessarily bullet-proof, but could be useful for some transformations.
36 | *
37 | * @param loc The path to process.
38 | */
39 | export function posix (loc: string): string {
40 | return loc.split(Path.sep).join('/')
41 | }
42 |
--------------------------------------------------------------------------------
/src/styles/mixins.scss:
--------------------------------------------------------------------------------
1 | // ========================================================================
2 | // Mixins
3 | // ========================================================================
4 |
5 | @mixin drop-shadow($opacity: .175) {
6 | box-shadow: 0 6px 12px rgba(0, 0, 0, $opacity);
7 | }
8 |
9 | @mixin no-shadow {
10 | background-clip: padding-box;
11 | box-shadow: none;
12 | }
13 |
14 | @mixin win32 {
15 | body.platform-win32 & {
16 | @content;
17 | }
18 | }
19 |
20 | @mixin win32-context {
21 | body.platform-win32 {
22 | @content;
23 | }
24 | }
25 |
26 | @mixin darwin {
27 | body.platform-darwin & {
28 | @content;
29 | }
30 | }
31 |
32 | @mixin darwin-context {
33 | body.platform-darwin {
34 | @content;
35 | }
36 | }
37 |
38 | @mixin linux {
39 | body.platform-linux & {
40 | @content;
41 | }
42 | }
43 |
44 | @mixin linux-context {
45 | body.platform-linux {
46 | @content;
47 | }
48 | }
49 |
50 | @mixin light {
51 | html[data-color-mode="light"] & {
52 | @content;
53 | }
54 | }
55 |
56 | @mixin light-context {
57 | html[data-color-mode="light"] {
58 | @content;
59 | }
60 | }
61 |
62 | @mixin dark {
63 | html[data-color-mode="dark"] & {
64 | @content;
65 | }
66 | }
67 |
68 | @mixin dark-context {
69 | html[data-color-mode="dark"] {
70 | @content;
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/src/renderer/components/Snippet.vue:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
10 |
51 |
--------------------------------------------------------------------------------
/src/renderer/components/modals/RemoveRepository.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Remove Repository
6 |
7 |
8 |
Are you sure you want to remove the repository from this project? This will not delete the repository from your filesystem and cannot be undone.
9 |
10 | The following repository will be removed:
11 |
12 | {{ repository.path }}
13 |
14 |
15 |
16 |
17 |
25 |
26 |
27 |
28 |
29 |
43 |
--------------------------------------------------------------------------------
/src/renderer/directives/markdown.js:
--------------------------------------------------------------------------------
1 | import { castArray } from 'lodash'
2 | import Strings from '@lib/helpers/strings'
3 |
4 | // Use an object for each binding so we can store original templates
5 | // in case dynamic content changes and we need to re-compute markup.
6 | class MarkdownDirective {
7 | constructor (helper, el, binding) {
8 | this.helper = helper
9 | this.template = (binding.modifiers.set ? false : binding.value) || el.innerText || el.textContent
10 | }
11 |
12 | html (el, binding) {
13 | let text = this.template
14 | // If set modifier is present, use value as replacers
15 | if (binding.modifiers.set) {
16 | text = this.helper.set(this.template, ...castArray(binding.value))
17 | } else if (binding.modifiers.plural) {
18 | text = this.helper.plural(this.template, binding.value)
19 | }
20 |
21 | return binding.modifiers.block
22 | ? this.helper.markdownBlock(text, true)
23 | : this.helper.markdown(text)
24 | }
25 | }
26 |
27 | export default function (locale = 'en-US') {
28 | return {
29 | beforeMount (el, binding, vnode) {
30 | el.markdown = new MarkdownDirective(new Strings(locale), el, binding)
31 | el.innerHTML = el.markdown.html(el, binding)
32 | },
33 | updated (el, binding, vnode) {
34 | el.innerHTML = el.markdown.html(el, binding)
35 | }
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/tests/mocks/electron.js:
--------------------------------------------------------------------------------
1 | const electron = {
2 | ipcRenderer: {
3 | listeners: {
4 | on: {},
5 | once: {}
6 | },
7 | on (event, callback) {
8 | electron.ipcRenderer.listeners.on[event] = callback
9 | },
10 | once (event, callback) {
11 | electron.ipcRenderer.listeners.once[event] = callback
12 | },
13 | send (channel, ...args) {},
14 | invoke (channel, ...args) {},
15 | removeAllListeners (channel) {},
16 |
17 | /**
18 | * Mimick a channel event coming from the main process.
19 | *
20 | * @param channel The channel in which the event is being triggered
21 | * @param args The arguments with which the event is being triggered
22 | */
23 | trigger (channel, ...args) {
24 | if (typeof electron.ipcRenderer.listeners.on[channel] === 'undefined') {
25 | throw Error(`Attempted to trigger unregistered event "${channel}"`)
26 | }
27 |
28 | const event = {}
29 | electron.ipcRenderer.listeners.on[channel](event, ...args)
30 | if (typeof electron.ipcRenderer.listeners.once[channel] !== 'undefined') {
31 | electron.ipcRenderer.listeners.once[channel](event, ...args)
32 | delete electron.ipcRenderer.listeners.once[channel]
33 | }
34 | }
35 | }
36 | }
37 |
38 | module.exports = electron
39 |
--------------------------------------------------------------------------------
/src/lib/process/errors.ts:
--------------------------------------------------------------------------------
1 | import { IProcess } from '@lib/process/process'
2 |
3 | /**
4 | * An error with a code number property.
5 | */
6 | export interface ErrorWithCode extends Error {
7 | code?: string | number | null
8 | }
9 |
10 | /**
11 | * An error thrown by our standard process.
12 | */
13 | export class ProcessError extends Error implements ErrorWithCode {
14 | process?: string
15 | code?: string | number | null | undefined
16 |
17 | /**
18 | * Set the process that originated the error.
19 | *
20 | * @param process The process to set to.
21 | */
22 | public setProcess (process: IProcess): this {
23 | this.process = process.toString()
24 | return this
25 | }
26 |
27 | /**
28 | * Get the process that originated the error as a plain
29 | * object, parsed from its string representation.
30 | *
31 | * @param process The process to set to.
32 | */
33 | public getProcess (): object | undefined {
34 | return this.process ? JSON.parse(this.process) : undefined
35 | }
36 |
37 | /**
38 | * The error code.
39 | *
40 | * @param code The error code we're setting.
41 | */
42 | public setCode (code?: string | number | null): this {
43 | this.code = code
44 | return this
45 | }
46 |
47 | /**
48 | * Transform this error to a string.
49 | */
50 | public toString (): string {
51 | return this.message
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/main/menu/test-menu.ts:
--------------------------------------------------------------------------------
1 | import { Menu } from '@main/menu'
2 | import { clipboard } from 'electron'
3 | import { ISuite } from '@lib/frameworks/suite'
4 | import { ITest } from '@lib/frameworks/test'
5 |
6 | export class TestMenu extends Menu {
7 | constructor (suite: ISuite, test: ITest, webContents: Electron.WebContents) {
8 | super(webContents)
9 |
10 | const originalName = test.getName() !== test.getDisplayName() ? test.getName() : ''
11 | this
12 | .add({
13 | label: __DARWIN__
14 | ? 'Copy Test Name'
15 | : 'Copy test name',
16 | click: () => {
17 | clipboard.writeText(test.getDisplayName() || test.getName())
18 | }
19 | })
20 | .addIf(!!originalName, {
21 | label: __DARWIN__
22 | ? 'Copy Original Test Name'
23 | : 'Copy original test name',
24 | click: () => {
25 | clipboard.writeText(originalName)
26 | }
27 | })
28 | .separator()
29 | .add({
30 | label: __DARWIN__
31 | ? 'Open Suite with Default Program'
32 | : 'Open suite with default program',
33 | click: () => {
34 | suite.open()
35 | },
36 | enabled: suite.canBeOpened()
37 | })
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/styles/app.scss:
--------------------------------------------------------------------------------
1 | @import "functions";
2 | @import "definitions";
3 | @import "mixins";
4 | @import "breakpoints";
5 | @import "vendor";
6 | @import "animations";
7 | @import "variables";
8 | @import "typography";
9 | @import "globals";
10 | @import "blocks/about";
11 | @import "blocks/ansi";
12 | @import "blocks/breadcrumbs";
13 | @import "blocks/buttons";
14 | @import "blocks/code";
15 | @import "blocks/collapsible";
16 | @import "blocks/console";
17 | @import "blocks/contents";
18 | @import "blocks/cta";
19 | @import "blocks/diff";
20 | @import "blocks/draggable";
21 | @import "blocks/feedback";
22 | @import "blocks/filename";
23 | @import "blocks/forms";
24 | @import "blocks/framework";
25 | @import "blocks/icons";
26 | @import "blocks/labels";
27 | @import "blocks/license";
28 | @import "blocks/loading";
29 | @import "blocks/meta";
30 | @import "blocks/modals";
31 | @import "blocks/nugget";
32 | @import "blocks/object";
33 | @import "blocks/status";
34 | @import "blocks/pre";
35 | @import "blocks/preferences";
36 | @import "blocks/project";
37 | @import "blocks/results";
38 | @import "blocks/scrollable";
39 | @import "blocks/selective";
40 | @import "blocks/settings";
41 | @import "blocks/sidebar";
42 | @import "blocks/tables";
43 | @import "blocks/terms";
44 | @import "blocks/test";
45 | @import "blocks/test-information";
46 | @import "blocks/test-result";
47 | @import "blocks/titlebar";
48 | @import "blocks/trace";
49 | @import "highlight/common";
50 | @import "highlight/light";
51 | @import "highlight/dark";
52 |
--------------------------------------------------------------------------------
/src/lib/frameworks/sort.ts:
--------------------------------------------------------------------------------
1 | import { get } from 'lodash'
2 |
3 | /**
4 | * The sort direction possibilities.
5 | */
6 | type SortDirection = 'asc' | 'desc'
7 |
8 | /**
9 | * A list of possible framework sort options.
10 | */
11 | export type FrameworkSort = 'framework' | 'name'
12 |
13 | export const sortOptions: { [key in FrameworkSort]: string } = {
14 | framework: 'Running order',
15 | name: 'Name'
16 | }
17 |
18 | export const sortDirections: { [key in FrameworkSort]: SortDirection } = {
19 | framework: 'asc',
20 | name: 'asc'
21 | }
22 |
23 | /**
24 | * Map a sort option to its display name.
25 | *
26 | * @param sort The sort option to map to a display name.
27 | */
28 | export function sortDisplayName (sort: FrameworkSort): string {
29 | return get(sortOptions, sort, 'Unknown sort')
30 | }
31 |
32 | /**
33 | * Return the reverse of a given direction.
34 | *
35 | * @param direction The direction to return the reverse of.
36 | */
37 | export function reverseDirection (direction: SortDirection): SortDirection {
38 | return direction === 'asc' ? 'desc' : 'asc'
39 | }
40 |
41 | /**
42 | * Map a sort option to its default direction.
43 | *
44 | * @param sort The sort option to map to a direction.
45 | * @param reverse Whether to reverse the default direction.
46 | */
47 | export function sortDirection (sort: FrameworkSort, reverse: boolean): SortDirection {
48 | const direction = get(sortDirections, sort, 'asc')
49 | return reverse ? reverseDirection(direction) : direction
50 | }
51 |
--------------------------------------------------------------------------------
/src/renderer/components/modals/RunningUnderTranslation.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Wrong architecture?
6 |
7 |
8 |
You are running Lode in an architecture that might not be appropriate to your system, which can cause it to be signifcantly slower. Please visit lode.run to check whether a more appropriate version is available.
9 |
10 |
11 | Do not show this message again
12 |
13 |
14 |
15 |
20 |
21 |
22 |
23 |
24 |
42 |
--------------------------------------------------------------------------------
/src/renderer/components/modals/ConfirmSwitchProject.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Confirm Switch Project
6 |
7 |
8 |
Are you sure you want to switch projects? Tests in the current project will be stopped and results will be reset.
9 |
10 |
11 | Do not show this message again
12 |
13 |
14 |
15 |
23 |
24 |
25 |
26 |
27 |
45 |
--------------------------------------------------------------------------------
/src/lib/reporters/phpunit-10/src/Util.php:
--------------------------------------------------------------------------------
1 | offsetExists($key);
21 | }
22 |
23 | return array_key_exists($key, $array);
24 | }
25 |
26 | /**
27 | * Safely get the value from an array using its key,
28 | * with an optional fallback
29 | *
30 | * @param \ArrayAccess|array $array
31 | * @param string $key
32 | * @param mixed $default
33 | * @return mixed
34 | */
35 | public static function get($array, $key, $default = null)
36 | {
37 | if (is_null($key)) {
38 | return $array;
39 | }
40 |
41 | if (static::exists($array, $key)) {
42 | return $array[$key];
43 | }
44 |
45 | return $default;
46 | }
47 |
48 | /**
49 | * Remove falsey values from an (associative) array.
50 | */
51 | public static function compact(array $array): array
52 | {
53 | foreach ($array as $key => $value) {
54 | if (!$value) {
55 | unset($array[$key]);
56 | }
57 | }
58 | return $array;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/tests/lib/frameworks/emitter.spec.js:
--------------------------------------------------------------------------------
1 | import { ApplicationWindow } from '@main/application-window'
2 | import { ProjectEventEmitter } from '@lib/frameworks/emitter'
3 |
4 | jest.mock('@lib/state')
5 | jest.mock('electron-store')
6 | jest.mock('@main/application-window')
7 |
8 | it('can return the associated application window', async () => {
9 | const window = new ApplicationWindow()
10 | const emitter = new ProjectEventEmitter(window)
11 | expect(emitter.getApplicationWindow()).toBe(window)
12 | })
13 |
14 | it('can emit events to the application window', async () => {
15 | const window = new ApplicationWindow()
16 | window.canReceiveEvents = jest.fn(() => true)
17 |
18 | const emitter = new ProjectEventEmitter(window)
19 |
20 | // Can emit with no arguments
21 | emitter.emitToRenderer('biscuit')
22 | expect(window.send).toHaveBeenLastCalledWith('biscuit', [])
23 |
24 | // Can emit with arguments
25 | emitter.emitToRenderer('biscuit', 'Hobnobs', 'Digestives', 'Rich Tea')
26 | expect(window.send).toHaveBeenLastCalledWith('biscuit', ['Hobnobs', 'Digestives', 'Rich Tea'])
27 |
28 | expect(window.send).toHaveBeenCalledTimes(2)
29 | })
30 |
31 | it('does not emit events if the application window is blocking them', async () => {
32 | const window = new ApplicationWindow()
33 | window.canReceiveEvents = jest.fn(() => false)
34 |
35 | const emitter = new ProjectEventEmitter(window)
36 | emitter.emitToRenderer('biscuit')
37 | expect(window.send).not.toHaveBeenCalled()
38 | })
39 |
--------------------------------------------------------------------------------
/src/lib/reporters/phpunit/src/Util.php:
--------------------------------------------------------------------------------
1 | offsetExists($key);
19 | }
20 |
21 | return array_key_exists($key, $array);
22 | }
23 |
24 | /**
25 | * Safely get the value from an array using its key,
26 | * with an optional fallback
27 | *
28 | * @param \ArrayAccess|array $array
29 | * @param string $key
30 | * @param mixed $default
31 | * @return mixed
32 | */
33 | public static function get($array, $key, $default = null)
34 | {
35 | if (is_null($key)) {
36 | return $array;
37 | }
38 |
39 | if (static::exists($array, $key)) {
40 | return $array[$key];
41 | }
42 |
43 | return $default;
44 | }
45 |
46 | /**
47 | * Remove falsey values from an (associative) array.
48 | *
49 | * @param array $array
50 | */
51 | public static function compact(array $array): array
52 | {
53 | foreach ($array as $key => $value) {
54 | if (!$value) {
55 | unset($array[$key]);
56 | }
57 | }
58 | return $array;
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/main/menu/file-menu.ts:
--------------------------------------------------------------------------------
1 | import { Menu } from '@main/menu'
2 | import { File } from '@main/file'
3 | import { clipboard } from 'electron'
4 |
5 | export class FileMenu extends Menu {
6 | constructor (filePath: string, webContents: Electron.WebContents) {
7 | super(webContents)
8 |
9 | this
10 | .add({
11 | id: 'reveal',
12 | label: __DARWIN__
13 | ? 'Reveal in Finder'
14 | : __WIN32__
15 | ? 'Show in Explorer'
16 | : 'Show in your File Manager',
17 | click: () => {
18 | File.reveal(filePath)
19 | },
20 | enabled: File.exists(filePath)
21 | })
22 | .add({
23 | id: 'copy',
24 | label: __DARWIN__
25 | ? 'Copy File Path'
26 | : 'Copy file path',
27 | click: () => {
28 | clipboard.writeText(filePath)
29 | },
30 | enabled: File.exists(filePath)
31 | })
32 | .add({
33 | id: 'open',
34 | label: __DARWIN__
35 | ? 'Open with Default Program'
36 | : 'Open with default program',
37 | click: () => {
38 | File.open(filePath)
39 | },
40 | enabled: File.isSafe(filePath) && File.exists(filePath)
41 | })
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/renderer/components/Collapsible.vue:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
50 |
--------------------------------------------------------------------------------
/workflow/reporters/webpack.jest.config.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | process.env.BABEL_ENV = 'reporters'
4 |
5 | const { getReplacements } = require('../app-info')
6 | const replacements = getReplacements()
7 |
8 | const path = require('path')
9 | const webpack = require('webpack')
10 |
11 | const config = {
12 | target: 'node',
13 | entry: {
14 | main: path.join(__dirname, '../../src/lib/reporters/jest/index.js')
15 | },
16 | output: {
17 | filename: 'index.js',
18 | library: {
19 | type: 'commonjs2'
20 | },
21 | path: path.join(__dirname, '../../static/reporters/jest')
22 | },
23 | resolve: {
24 | extensions: ['.js']
25 | },
26 | module: {
27 | rules: [
28 | {
29 | test: /\.js$/,
30 | use: 'babel-loader',
31 | exclude: /node_modules/
32 | }
33 | ]
34 | },
35 | node: {
36 | __dirname: process.env.NODE_ENV !== 'production',
37 | __filename: process.env.NODE_ENV !== 'production'
38 | },
39 | plugins: [
40 | new webpack.NoEmitOnErrorsPlugin(),
41 | new webpack.DefinePlugin(replacements)
42 | ],
43 | optimization: {
44 | minimize: process.env.NODE_ENV === 'production'
45 | },
46 | }
47 |
48 | if (process.env.NODE_ENV === 'production') {
49 | config.plugins.push(
50 | new webpack.DefinePlugin({
51 | 'process.env.NODE_ENV': '"production"'
52 | })
53 | )
54 | }
55 |
56 | module.exports = config
57 |
--------------------------------------------------------------------------------
/workflow/webpack.main.config.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | process.env.BABEL_ENV = 'main'
4 |
5 | const { getReplacements } = require('./app-info')
6 |
7 | const base = require('./webpack.base.config.js')
8 | const _ = require('lodash')
9 | const path = require('path')
10 | const { dependencies } = require('../package.json')
11 | const webpack = require('webpack')
12 |
13 | const mainConfig = {
14 | ...base,
15 | target: 'electron-main',
16 | entry: {
17 | main: path.join(__dirname, '../src/main/index.ts')
18 | },
19 | externals: [
20 | ...Object.keys(dependencies || {})
21 | ],
22 | node: {
23 | __dirname: process.env.NODE_ENV !== 'production',
24 | __filename: process.env.NODE_ENV !== 'production'
25 | },
26 | plugins: [
27 | new webpack.NoEmitOnErrorsPlugin(),
28 | new webpack.DefinePlugin(Object.assign({}, getReplacements(), {
29 | __PROCESS_KIND__: JSON.stringify('main')
30 | }))
31 | ]
32 | }
33 |
34 | if (process.env.NODE_ENV !== 'production' || process.env.IS_DEV) {
35 | mainConfig.plugins.push(
36 | new webpack.DefinePlugin({
37 | '__static': `"${path.join(__dirname, '../static').replace(/\\/g, '\\\\')}"`
38 | })
39 | )
40 | }
41 |
42 | if (process.env.NODE_ENV === 'production') {
43 | Array.prototype.push.apply(mainConfig.plugins, _.compact([
44 | new webpack.DefinePlugin({
45 | 'process.env.NODE_ENV': '"production"'
46 | })
47 | ]))
48 | }
49 |
50 | module.exports = mainConfig
51 |
--------------------------------------------------------------------------------
/src/renderer/store/modules/modals.js:
--------------------------------------------------------------------------------
1 | import { last } from 'lodash'
2 |
3 | export default {
4 | namespaced: true,
5 | state: {
6 | modals: []
7 | },
8 | mutations: {
9 | ADD (state, name) {
10 | state.modals.push(name)
11 | },
12 | REMOVE (state) {
13 | state.modals.pop()
14 | },
15 | CLEAR (state) {
16 | state.modals = []
17 | }
18 | },
19 | actions: {
20 | open: ({ state, commit, dispatch, getters }, name) => {
21 | if (!getters['isOpen'](name)) {
22 | commit('ADD', name)
23 | dispatch('change')
24 | }
25 | },
26 | close: ({ state, commit, dispatch }) => {
27 | commit('REMOVE')
28 | dispatch('change')
29 | },
30 | clear: ({ state, commit, dispatch }) => {
31 | commit('CLEAR')
32 | dispatch('change')
33 | },
34 | change: ({ state, commit }) => {
35 | if (state.modals.length) {
36 | document.body.classList.add('modal-open')
37 | return
38 | }
39 | document.body.classList.remove('modal-open')
40 | }
41 | },
42 | getters: {
43 | isOpen: state => name => {
44 | return last(state.modals) === name
45 | },
46 | hasModals: state => {
47 | return state.modals.length > 0
48 | },
49 | modals: state => {
50 | return state.modals
51 | }
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/src/lib/reporters/phpunit-10/src/Status.php:
--------------------------------------------------------------------------------
1 | self::EMPTY,
24 | Test\DeprecationTriggered::class => self::WARNING,
25 | Test\Errored::class => self::FAILED,
26 | Test\ErrorTriggered::class => self::WARNING,
27 | Test\Failed::class => self::FAILED,
28 | Test\MarkedIncomplete::class => self::INCOMPLETE,
29 | Test\NoticeTriggered::class => self::WARNING,
30 | Test\Passed::class => self::PASSED,
31 | Test\PhpDeprecationTriggered::class => self::WARNING,
32 | Test\PhpNoticeTriggered::class => self::WARNING,
33 | Test\PhpunitDeprecationTriggered::class => self::WARNING,
34 | Test\PhpunitErrorTriggered::class => self::WARNING,
35 | Test\PhpunitWarningTriggered::class => self::WARNING,
36 | Test\PhpWarningTriggered::class => self::WARNING,
37 | Test\Skipped::class => self::SKIPPED,
38 | Test\WarningTriggered::class => self::WARNING,
39 | };
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/styles/blocks/test-result.scss:
--------------------------------------------------------------------------------
1 | .test-result {
2 | display: flex;
3 | flex: 1;
4 | flex-direction: column;
5 | flex-grow: 1;
6 | min-height: 0;
7 |
8 | .tabs {
9 | background-color: var(--secondary-background-color);
10 | border-bottom: 1px solid var(--primary-border-color);
11 |
12 | nav {
13 | padding: 0 var(--spacing-half);
14 |
15 | .tab {
16 | appearance: none;
17 | background: none;
18 | border: 0;
19 | box-shadow: none;
20 | color: var(--color-fg-default);
21 | padding: calc(var(--spacing) + -2px) var(--spacing);
22 |
23 | &:hover,
24 | &:focus,
25 | &:active {
26 | outline: none;
27 | text-decoration: none;
28 | }
29 |
30 | &.selected {
31 | color: var(--color-link);
32 | }
33 | }
34 | }
35 | }
36 |
37 | .test-result-general {
38 | user-select: text;
39 | }
40 |
41 | .test-result-breakdown {
42 | flex: 1;
43 | overflow-x: hidden;
44 | overflow-y: auto;
45 | padding: 15px;
46 | position: relative;
47 |
48 | // Avoid content inside results pane growing indefinitely,
49 | // especially when trace headers have very long filenames
50 | // which are set to not wrap.
51 | > div {
52 | position: absolute;
53 | width: calc(100% - 30px);
54 | }
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/lib/process/factory.ts:
--------------------------------------------------------------------------------
1 | import { find } from 'lodash'
2 | import { ProcessId, ProcessOptions, IProcess, DefaultProcess } from '@lib/process/process'
3 | import { Runners } from '@lib/process/runners'
4 | import pool from '@lib/process/pool'
5 |
6 | export class ProcessFactory {
7 | /**
8 | * Make a new process according to the given options.
9 | *
10 | * @param options The options for the process we're making.
11 | * @param poolId An optional id with which the newly made process will be added to the pool.
12 | */
13 | public static make (options: ProcessOptions, poolId?: ProcessId): IProcess {
14 | let spawned: IProcess | null = null
15 |
16 | if (options.forceRunner) {
17 | // If a runner has been pre-determined, try to find it within list of
18 | // existing runners and create a process with it, if possible.
19 | const Runner = find(Runners, runner => runner.type === options.forceRunner)
20 | if (Runner) {
21 | spawned = new Runner(options)
22 | }
23 | } else {
24 | // If no runner was specificed, we'll try to determine which runner to
25 | // use by feeding each of them the command.
26 | for (let i = 0; i < Runners.length; i++) {
27 | if (Runners[i].owns(options.command)) {
28 | spawned = new Runners[i](options)
29 | }
30 | }
31 | }
32 |
33 | spawned = spawned || new DefaultProcess(options)
34 |
35 | pool.add(spawned, poolId)
36 |
37 | return spawned
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/src/renderer/components/Filename.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ dir }}{{ name }}
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
59 |
--------------------------------------------------------------------------------
/workflow/build.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | process.env.NODE_ENV = 'production'
4 |
5 | const del = require('del')
6 | const webpack = require('webpack')
7 |
8 | const mainConfig = require('./webpack.main.config')
9 | const preloadConfig = require('./webpack.preload.config')
10 | const rendererConfig = require('./webpack.renderer.config')
11 |
12 | build()
13 |
14 | function build () {
15 | del.sync(['dist/*', '!.gitkeep'])
16 | pack(mainConfig, 'main')
17 | pack(preloadConfig, 'preload')
18 | pack(rendererConfig, 'renderer')
19 | }
20 |
21 | function pack (config, input) {
22 | return new Promise((resolve, reject) => {
23 | config.mode = process.env.NODE_ENV === 'development' ? 'development' : 'production'
24 | config.plugins = [...config.plugins, new webpack.ProgressPlugin({
25 | handler (percentage, msg) {
26 | console.log(`${input}/${msg}: ${(percentage * 100).toFixed()}%`)
27 | }
28 | })]
29 | webpack(config, (err, stats) => {
30 | if (err) reject(err.stack || err)
31 | else if (stats.hasErrors()) {
32 | let err = ''
33 |
34 | stats.toString({
35 | chunks: false,
36 | colors: true
37 | })
38 | .split(/\r?\n/)
39 | .forEach(line => {
40 | err += ` ${line}\n`
41 | })
42 |
43 | reject(err)
44 | } else {
45 | resolve(stats.toString({
46 | chunks: false,
47 | colors: true
48 | }))
49 | }
50 | })
51 | })
52 | }
53 |
--------------------------------------------------------------------------------
/src/styles/definitions.scss:
--------------------------------------------------------------------------------
1 | // ========================================================================
2 | // Colors
3 | // ========================================================================
4 |
5 | //
6 | //
7 | // -------- Grays --------
8 | $gray-000: #f7f7f7 !default;
9 | $gray-100: #f0f0f0 !default;
10 | $gray-200: #ececec !default;
11 | $gray-300: #e3e3e3 !default;
12 | $gray-400: #ced4da !default;
13 | $gray-500: #adb5bd !default;
14 | $gray-600: #6c757d !default;
15 | $gray-700: #495057 !default;
16 | $gray-800: #343a40 !default;
17 | $gray-900: #212529 !default;
18 |
19 | // -------- Blue --------
20 | $blue-000: #f1f8ff !default;
21 | $blue-100: #dfefff !default;
22 | $blue-200: #bbdaff !default;
23 | $blue-300: #79b8ff !default;
24 | $blue-400: #2188ff !default;
25 | $blue-500: #0366d6 !default;
26 | $blue-600: #005cc5 !default;
27 | $blue-700: #044289 !default;
28 | $blue-800: #032f62 !default;
29 | $blue-900: #05264c !default;
30 |
31 | // -------- Yellow --------
32 | $yellow-000: #fffdef !default;
33 | $yellow-100: #fdf7ce !default;
34 | $yellow-200: #fff29c !default;
35 | $yellow-300: #ffea7f !default;
36 | $yellow-400: #ffdf5d !default;
37 | $yellow-500: #ffd33d !default;
38 | $yellow-600: #f9c513 !default;
39 | $yellow-700: #dbab09 !default;
40 | $yellow-800: #b08800 !default;
41 | $yellow-900: #735c0f !default;
42 |
43 | // -------- Red --------
44 | $red-000: #ffeef0 !default;
45 | $red-100: #ffdce0 !default;
46 | $red-200: #fdaeb7 !default;
47 | $red-300: #f97583 !default;
48 | $red-400: #f8575f !default;
49 | $red-500: #f7434c !default;
50 | $red-600: #f6212b !default;
51 | $red-700: #e40a15 !default;
52 | $red-800: #c20912 !default;
53 | $red-900: #a4070f !default;
54 |
55 | // -------- Fades --------
56 | $white: #fff !default;
57 |
--------------------------------------------------------------------------------
/src/renderer/helpers/validator.js:
--------------------------------------------------------------------------------
1 | /**
2 | * A generic validation class.
3 | */
4 | export default class Validator {
5 | /**
6 | * Whether the current instance is valid.
7 | * @param ValidationErrors errors
8 | */
9 | constructor (errors) {
10 | this.errors = errors || {}
11 | }
12 |
13 | refresh (errors) {
14 | this.errors = errors || {}
15 | }
16 |
17 | /**
18 | * Whether the current instance is valid.
19 | */
20 | isValid () {
21 | return this.hasErrors()
22 | }
23 |
24 | /**
25 | * Reset errors in the current instance.
26 | */
27 | reset (fields) {
28 | Object.keys(this.errors).forEach(key => {
29 | if (!fields || fields.includes(key)) {
30 | this.errors[key] = []
31 | }
32 | })
33 | }
34 |
35 | /**
36 | * Whether the current instance has any errors for the given key.
37 | *
38 | * @param key The key to check for errors.
39 | */
40 | hasErrors (key) {
41 | if (typeof key === 'undefined') {
42 | let hasErrors = true
43 | Object.keys(this.errors).forEach(key => {
44 | if (this.errors[key].length > 0) {
45 | hasErrors = false
46 | }
47 | })
48 | return hasErrors
49 | }
50 |
51 | return this.errors[key] && this.errors[key].length > 0
52 | }
53 |
54 | /**
55 | * Get errors for the given key.
56 | *
57 | * @param key The key to get errors from.
58 | */
59 | getErrors (key) {
60 | if (!this.hasErrors(key)) {
61 | return ''
62 | }
63 |
64 | return this.errors[key].join('; ')
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/workflow/cypress.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const Path = require('node:path')
4 | const { exec } = require('child_process')
5 | let callbackId = null
6 |
7 | const teardown = (code = 0) => {
8 | if (callbackId) {
9 | try {
10 | // Try to kill child process, but don't
11 | // throw if it no longer exists
12 | process.kill(-callbackId)
13 | } catch (_) {}
14 | }
15 | process.exit(code)
16 | }
17 |
18 | if (process.platform === 'win32') {
19 | const rl = require('readline').createInterface({
20 | input: process.stdin,
21 | output: process.stdout
22 | })
23 |
24 | rl.on('SIGINT', () => {
25 | process.emit('SIGINT')
26 | })
27 | }
28 |
29 | process.on('SIGINT', () => {
30 | teardown()
31 | })
32 |
33 | const startRenderer = require('./runners').startRenderer
34 | startRenderer().then(() => {
35 | const callback = exec(
36 | process.argv[2] === 'open'
37 | ? `cypress open ${process.argv.slice(3).join(' ')} --config-file ${Path.resolve(__dirname, '../tests/cypress/config.ts')}`
38 | : `cypress run -b electron ${process.argv.slice(3).join(' ')} --config-file ${Path.resolve(__dirname, '../tests/cypress/config.ts')}`,
39 | {
40 | env: {
41 | ...process.env,
42 | FORCE_COLOR: 3
43 | }
44 | }
45 | )
46 | callbackId = callback.pid
47 | callback.stdout.setEncoding('utf8')
48 | callback.stderr.setEncoding('utf8')
49 | callback.stdout.pipe(process.stdout)
50 | callback.stderr.pipe(process.stderr)
51 | callback.on('error', (...args) => {
52 | teardown(...args)
53 | })
54 | callback.on('close', (...args) => {
55 | teardown(...args)
56 | })
57 | })
58 |
--------------------------------------------------------------------------------
/src/styles/functions.scss:
--------------------------------------------------------------------------------
1 | // ========================================================================
2 | // Functions
3 | // ========================================================================
4 |
5 | @use "sass:math";
6 | @use "sass:map";
7 | @use "sass:string";
8 |
9 | ///
10 | /// Casts a number into a string
11 | ///
12 | /// @param {String | Number} $value - Value to be parsed
13 | ///
14 | /// @return {String}
15 | ///
16 | @function to-string($value) {
17 | @return inspect($value);
18 | }
19 |
20 | ///
21 | /// Casts a string into a number
22 | ///
23 | /// @param {String | Number} $value - Value to be parsed
24 | ///
25 | /// @return {Number}
26 | ///
27 | @function to-number($value) {
28 | @if type-of($value) == "number" {
29 | @return $value;
30 | } @else if type-of($value) != "string" {
31 | $log: log("Value for `to-number` should be a number or a string.");
32 | }
33 |
34 | $result: 0;
35 | $digits: 0;
36 | $minus: string.slice($value, 1, 1) == "-";
37 | $numbers: ("0": 0, "1": 1, "2": 2, "3": 3, "4": 4, "5": 5, "6": 6, "7": 7, "8": 8, "9": 9);
38 |
39 | @for $i from if($minus, 2, 1) through str-length($value) {
40 | $character: string.slice($value, $i, $i);
41 |
42 | @if not (index(map-keys($numbers), $character) or $character == ".") {
43 | @return to-length(if($minus, -$result, $result), string.slice($value, $i));
44 | }
45 |
46 | @if $character == "." {
47 | $digits: 1;
48 | } @else if $digits == 0 {
49 | $result: $result * 10 + map.get($numbers, $character);
50 | } @else {
51 | $digits: $digits * 10;
52 | $result: $result + math.div(map.get($numbers, $character), $digits);
53 | }
54 | }
55 |
56 | @return if($minus, -$result, $result);
57 | }
58 |
--------------------------------------------------------------------------------
/src/renderer/components/modals/Licenses.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
An error ocurred while loading third party licenses. Please contact the authors for licensing information.
6 |
7 |
8 |
The following sets forth attribution notices for third party software that may be contained in portions of the Lode application. We thank the open source community for all of their contributions.
9 |
10 |
11 |
{{ license.id }}
12 |
{{ license.license }}
13 |
14 |
15 |
16 |
17 |
22 |
23 |
24 |
25 |
26 |
47 |
--------------------------------------------------------------------------------
/src/lib/process/pool.ts:
--------------------------------------------------------------------------------
1 | import { ProcessId, IProcess } from '@lib/process/process'
2 |
3 | /**
4 | * A pool of running processes.
5 | */
6 | class ProcessPool {
7 | public readonly processes: { [type in ProcessId]: IProcess } = {}
8 |
9 | /**
10 | * Add a new process to the pool.
11 | *
12 | * @param process The process being pooled.
13 | * @param id An optional id with which process will be added to the pool.
14 | */
15 | public add (process: IProcess, id?: ProcessId): void {
16 | // If id was given, we'll use it, otherwise we'll
17 | // try to get it from the process itself.
18 | if (!id) {
19 | id = process.getId()
20 |
21 | // If we can't acquire the id, don't pool it.
22 | if (!id) {
23 | return
24 | }
25 | }
26 |
27 | this.processes[id!] = process
28 |
29 | // Once the process closes, remove it from the pool.
30 | process.on('close', () => {
31 | this.remove(id!)
32 | })
33 | }
34 |
35 | /**
36 | * Find a pooled and running process using its id.
37 | *
38 | * @param id The id of the process trying to be found.
39 | */
40 | public findProcess (id: ProcessId): IProcess | undefined {
41 | return this.processes[id]
42 | }
43 |
44 | /**
45 | * Remove a process from the pool by its id.
46 | */
47 | public remove (id: ProcessId): void {
48 | if (typeof this.processes[id] !== 'undefined') {
49 | delete this.processes[id]
50 | }
51 | }
52 |
53 | /**
54 | * Clear the process pool.
55 | */
56 | public clear (): void {
57 | Object.keys(this.processes).forEach(id => {
58 | this.remove(id)
59 | })
60 | }
61 | }
62 |
63 | const pool = new ProcessPool()
64 |
65 | export default pool
66 |
--------------------------------------------------------------------------------
/src/renderer/components/Console.vue:
--------------------------------------------------------------------------------
1 |
2 |
6 |
12 |
13 |
14 | {{ $string.set('Line :0', output.line) }}
15 | {{ output.type }}
16 |
17 |
18 |
19 |
20 | {{ output.content }}
21 |
22 |
23 |
24 |
25 |
54 |
--------------------------------------------------------------------------------
/src/lib/reporters/phpunit-10/src/Lode.php:
--------------------------------------------------------------------------------
1 | share(new Console);
29 | }
30 |
31 | /**
32 | * Resolve the given type from the container. Will instatiate
33 | * and set the container if it's not yet available.
34 | *
35 | * @param string $abstract
36 | * @return mixed
37 | */
38 | public static function make($abstract)
39 | {
40 | if (is_null(static::$instance)) {
41 | static::$instance = new static;
42 | }
43 |
44 | return call_user_func_array([static::$instance, 'resolve'], [$abstract]);
45 | }
46 |
47 | /**
48 | * Normalize a class name.
49 | */
50 | private function normalizeName($className): string
51 | {
52 | return ltrim(strtolower($className), '\\');
53 | }
54 |
55 | /**
56 | * Share an instance through the container.
57 | */
58 | private function share($obj): void
59 | {
60 | $normalizedName = $this->normalizeName(get_class($obj));
61 | $this->shares[$normalizedName] = $obj;
62 | }
63 |
64 | /**
65 | * Return th shared class from the container.
66 | */
67 | private function resolve($abstract): mixed
68 | {
69 | $normalizedClass = $this->normalizeName($abstract);
70 | if (isset($this->shares[$normalizedClass])) {
71 | return $this->shares[$normalizedClass];
72 | }
73 |
74 | return null;
75 | }
76 | }
77 |
--------------------------------------------------------------------------------
/src/styles/blocks/project.scss:
--------------------------------------------------------------------------------
1 | .project {
2 | display: flex;
3 | flex-grow: 1;
4 |
5 | > .split {
6 | display: flex;
7 | flex-grow: 1;
8 |
9 | > .pane {
10 | display: flex;
11 | flex-direction: column;
12 | flex-grow: 1;
13 | height: 100%;
14 | overflow: hidden;
15 | position: relative;
16 | width: 33%;
17 | z-index: 1;
18 |
19 | + .gutter {
20 | border-left: 1px solid var(--color-sidebar-border);
21 | cursor: col-resize !important;
22 | margin: 0;
23 | margin-right: calc(calc(var(--pane-gutter-width) - 1px) * -1);
24 | z-index: 3;
25 | }
26 |
27 | &:not(.sidebar) {
28 | + .gutter {
29 | border-left-color: var(--pane-border-color);
30 | }
31 | }
32 | }
33 |
34 | &.empty {
35 | > .pane:not(.sidebar) {
36 | background-color: var(--secondary-background-color);
37 | overflow: visible;
38 | white-space: nowrap;
39 | z-index: 2;
40 |
41 | .loading,
42 | .cta {
43 | align-items: flex-start;
44 | margin-left: calc(var(--spacing-triple) - var(--pane-gutter-width));
45 | padding-top: var(--spacing-double);
46 |
47 | .btn {
48 | margin-top: var(--spacing);
49 | }
50 | }
51 |
52 | + .gutter {
53 | visibility: hidden;
54 | }
55 | }
56 |
57 | > :last-child {
58 | border-left: 0;
59 | margin-left: -1px;
60 | z-index: 1 !important;
61 | }
62 | }
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/renderer/plugins/modals.js:
--------------------------------------------------------------------------------
1 | export default class Modals {
2 | constructor (store) {
3 | this.store = store
4 | this.modals = []
5 | }
6 |
7 | install (app) {
8 | app.config.globalProperties.$modal = this
9 | }
10 |
11 | open (name, properties = {}, callback = null) {
12 | this.store.dispatch('modals/open', name)
13 | this.modals.push({ properties, callback })
14 | }
15 |
16 | confirm (name, properties = {}) {
17 | return new Promise((resolve, reject) => {
18 | this.store.dispatch('modals/open', name)
19 | this.modals.push({ properties: { ...properties, ...{ resolve, reject }}})
20 | })
21 | }
22 |
23 | confirmIf (condition, name, properties = {}) {
24 | if (typeof condition === 'function') {
25 | condition = condition()
26 | }
27 | // If no confirmation is required, return a promise that resolves
28 | // automatically, for consistency.
29 | return condition ? this.confirm(name, properties) : new Promise((resolve, reject) => {
30 | resolve()
31 | })
32 | }
33 |
34 | close () {
35 | this.store.dispatch('modals/close')
36 | const modal = this.modals.pop()
37 | if (modal.callback) {
38 | // Set a timeout before triggering callback in case callback is going
39 | // to instantiate a similar modal. Not doing so could cause the modal
40 | // to be cached by Vue, thus not rendering properly (i.e. not calling
41 | // `created` or `mounted` lifecycle events on the new modal).
42 | setTimeout(() => {
43 | modal.callback.call()
44 | })
45 | }
46 | }
47 |
48 | clear () {
49 | this.store.dispatch('modals/clear')
50 | this.modals = []
51 | }
52 |
53 | getProperties (index) {
54 | return this.modals[index].properties
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/tests/cypress/config.ts:
--------------------------------------------------------------------------------
1 | import * as Path from 'node:path'
2 | import { defineConfig } from 'cypress'
3 | import webpackPreprocessor from '@cypress/webpack-preprocessor'
4 |
5 | export default defineConfig({
6 | e2e: {
7 | projectId: '6ki1wz',
8 | baseUrl: 'http://localhost:9080',
9 | specPattern: 'tests/cypress/e2e/**/*.{js,ts}',
10 | fixturesFolder: Path.resolve('../fixtures'),
11 | screenshotsFolder: Path.resolve('screenshots'),
12 | supportFile: Path.resolve('support/index.js'),
13 | videosFolder: Path.resolve('videos'),
14 | videoCompression: 0,
15 | video: true,
16 | retries: {
17 | runMode: 2,
18 | openMode: 0
19 | },
20 | scrollBehavior: false,
21 | waitForAnimations: false,
22 | setupNodeEvents (on, config) {
23 | on('file:preprocessor', webpackPreprocessor({
24 | webpackOptions: {
25 | module: {
26 | rules: [
27 | {
28 | test: /\.ts?$/,
29 | loader: 'ts-loader',
30 | exclude: /node_modules/
31 | },
32 | {
33 | test: /\.js$/,
34 | use: 'babel-loader',
35 | exclude: /node_modules/
36 | }
37 | ]
38 | },
39 | resolve: {
40 | alias: {
41 | '@preload': Path.join(__dirname, '../../src/preload'),
42 | 'electron': Path.join(__dirname, '../mocks/electron.js')
43 | },
44 | extensions: ['.js', '.ts']
45 | }
46 | }
47 | }))
48 | }
49 | }
50 | })
51 |
--------------------------------------------------------------------------------
/workflow/webpack.base.config.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | const path = require('path')
4 | const webpack = require('webpack')
5 | const ESLintPlugin = require('eslint-webpack-plugin')
6 | const StyleLintPlugin = require('stylelint-webpack-plugin')
7 |
8 | module.exports = {
9 | output: {
10 | filename: '[name].js',
11 | library: {
12 | type: 'umd'
13 | },
14 | path: path.join(__dirname, '../dist')
15 | },
16 | resolve: {
17 | alias: {
18 | '@': path.join(__dirname, '../src/renderer'),
19 | '@lib': path.join(__dirname, '../src/lib'),
20 | '@main': path.join(__dirname, '../src/main'),
21 | 'vue$': 'vue/dist/vue.esm-bundler.js'
22 | },
23 | extensions: ['.js', '.ts']
24 | },
25 | module: {
26 | rules: [
27 | {
28 | test: /\.ts?$/,
29 | loader: 'ts-loader',
30 | options: {
31 | appendTsSuffixTo: [/\.vue$/]
32 | },
33 | exclude: /node_modules/
34 | },
35 | {
36 | test: /\.js$/,
37 | use: 'babel-loader',
38 | exclude: /node_modules/
39 | },
40 | {
41 | test: /\.js$/,
42 | use: 'source-map-loader',
43 | enforce: 'pre'
44 | }
45 | ]
46 | },
47 | optimization: {
48 | minimize: false,
49 | removeEmptyChunks: true
50 | },
51 | devtool: process.env.NODE_ENV !== 'production' ? 'source-map' : false,
52 | plugins: [
53 | new ESLintPlugin({
54 | quiet: true
55 | }),
56 | new StyleLintPlugin({
57 | files: ['src/**/*.{vue,scss}']
58 | }),
59 | new webpack.DefinePlugin({
60 | __VUE_OPTIONS_API__: true,
61 | __VUE_PROD_DEVTOOLS__: false
62 | })
63 | ],
64 | stats: 'errors-only'
65 | }
66 |
--------------------------------------------------------------------------------
/src/lib/process/runners/npm.ts:
--------------------------------------------------------------------------------
1 | import { compact, concat, drop } from 'lodash'
2 | import { IProcessEnvironment, IProcess, DefaultProcess } from '@lib/process/process'
3 |
4 | export class NpmProcess extends DefaultProcess implements IProcess {
5 | static readonly type: string = 'npm'
6 |
7 | /**
8 | * Whether this process owns a given command.
9 | *
10 | * @param command The command we're checking to match an NPM runner.
11 | */
12 | public static owns (command: string): boolean {
13 | return command.toLowerCase().search(/\bnpm(\.cmd)?(?!\.) run\b/) > -1
14 | }
15 |
16 | /**
17 | * Return the array of arguments with which to spawn the child process.
18 | * NPM requires arguments to be preceded by '--', so this is where we
19 | * will enforce that syntax. We also need to patch the binary path for
20 | * Windows environments.
21 | */
22 | protected spawnArguments (args: Array): Array {
23 | if (!args.length) {
24 | return args
25 | }
26 |
27 | let binary = args.shift()
28 | if (this.platform === 'win32' && binary === 'npm') {
29 | binary = 'npm.cmd'
30 | }
31 |
32 | // Drop the "run", which we don't need to manipulate.
33 | args = drop(args, 1)
34 |
35 | // First argument after npm run is our script, so shift it.
36 | const script = args.shift()
37 |
38 | // Recreate the arguments by prefixing remaining ones with '--'.
39 | return compact(concat(binary!, 'run', (script || ''), args.length ? '--' : '', args))
40 | }
41 |
42 | /**
43 | * Return the env object with which to spawn the child process.
44 | */
45 | protected spawnEnv (env: IProcessEnvironment): IProcessEnvironment {
46 | return {
47 | ...env,
48 | ...{
49 | // Disable npm update notifier
50 | NO_UPDATE_NOTIFIER: 1
51 | }
52 | }
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/tests/cypress/support/assertions.js:
--------------------------------------------------------------------------------
1 | import electron from '../../mocks/electron'
2 |
3 | Cypress.Commands.add('assertInvoked', (...args) => {
4 | expect(electron.ipcRenderer.invoke).to.be.calledWith(...args)
5 | })
6 |
7 | Cypress.Commands.add('assertInvokedOnce', (...args) => {
8 | expect(electron.ipcRenderer.invoke).to.be.calledOnceWith(...args)
9 | })
10 |
11 | Cypress.Commands.add('assertInvokedCount', times => {
12 | expect(electron.ipcRenderer.invoke).to.be.callCount(times)
13 | })
14 |
15 | Cypress.Commands.add('assertEmitted', (...args) => {
16 | expect(electron.ipcRenderer.send).to.be.calledWith(...args)
17 | })
18 |
19 | Cypress.Commands.add('assertEmittedOnce', (...args) => {
20 | expect(electron.ipcRenderer.send).to.be.calledOnceWith(...args)
21 | })
22 |
23 | Cypress.Commands.add('assertEmittedCount', times => {
24 | expect(electron.ipcRenderer.send).to.be.calledOnceWith(times)
25 | })
26 |
27 | Cypress.Commands.add('assertArgs', { prevSubject: true }, (subject, ...args) => {
28 | cy.then(() => {
29 | return new Promise((resolve) => {
30 | expect(subject.args).to.deep.equal(args)
31 | resolve()
32 | })
33 | })
34 | })
35 |
36 | Cypress.Commands.add('assertArgEq', { prevSubject: true }, (subject, index, value) => {
37 | cy.then(() => {
38 | return new Promise((resolve) => {
39 | expect(subject.args[index]).to.deep.equal(value)
40 | resolve()
41 | })
42 | })
43 | })
44 |
45 | Cypress.Commands.add('assertChannel', { prevSubject: true }, (subject, channel) => {
46 | cy.then(() => {
47 | return new Promise((resolve) => {
48 | expect(subject.args[0]).to.equal(channel)
49 | resolve()
50 | })
51 | })
52 | })
53 |
54 | Cypress.Commands.add('assertNormalizedText', { prevSubject: true }, (subject, string) => {
55 | cy
56 | .wrap(subject)
57 | .should(el => {
58 | expect(el.get(0).innerText.trim()).to.eq(string)
59 | })
60 | })
61 |
--------------------------------------------------------------------------------
/src/lib/process/search.ts:
--------------------------------------------------------------------------------
1 | export class BufferedSearch {
2 | terms: {
3 | [index: string]: {
4 | buffer: string,
5 | matched: boolean
6 | }
7 | } = {}
8 |
9 | term (term: string, string: string): boolean {
10 | // Have we started looking for this yet? If not, prepare buffer.
11 | if (!this.terms[term]) {
12 | this.terms[term] = {
13 | buffer: '',
14 | matched: false
15 | }
16 | } else if (this.terms[term].matched) {
17 | return true
18 | }
19 |
20 | // Strip string of whitespace and append to buffer
21 | this.terms[term].buffer += string.replace(/\s+/g, '')
22 |
23 | let search = ''
24 | const characters = term.split('')
25 | for (let i = 0; i < characters.length; i++) {
26 | // Create search term substring
27 | search += characters[i]
28 |
29 | // If length of term substring exceeds that of the buffer, return.
30 | // Buffer will be kept and next call might yield a result.
31 | if (search.length > this.terms[term].buffer.length) {
32 | return false
33 | }
34 |
35 | // Search for term substring inside buffer
36 | const index = this.terms[term].buffer.indexOf(search)
37 |
38 | if (index === -1) {
39 | // If it doesn't match, return false and discard buffer
40 | this.terms[term].buffer = ''
41 | this.terms[term].matched = false
42 | return false
43 | } else if (index > 0) {
44 | // If it matches beyond start, discard content preceding match
45 | this.terms[term].buffer = this.terms[term].buffer.substring(index)
46 | }
47 | }
48 |
49 | // If full length of term was iterated through and matches occurred
50 | // consistently in the buffer, return true.
51 | return true
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/tests/lib/process/pool.spec.js:
--------------------------------------------------------------------------------
1 | import { EventEmitter } from 'events'
2 | import pool from '@lib/process/pool'
3 |
4 | beforeEach(() => {
5 | pool.clear()
6 | })
7 |
8 | it('can pool processes without specifying id', () => {
9 | const spawned = {
10 | getId: jest.fn().mockReturnValue(7),
11 | on: jest.fn()
12 | }
13 | pool.add(spawned)
14 | expect(pool.processes[7]).toBe(spawned)
15 | expect(spawned.getId).toHaveBeenCalledTimes(1)
16 | expect(spawned.on).toHaveBeenCalledTimes(1)
17 | })
18 |
19 | it('does not pool processes if it cannot figure out the process id', () => {
20 | const spawned = {
21 | getId: jest.fn().mockReturnValue(null),
22 | on: jest.fn()
23 | }
24 | pool.add(spawned)
25 | expect(pool.processes).toEqual({})
26 | expect(spawned.getId).toHaveBeenCalledTimes(1)
27 | expect(spawned.on).not.toHaveBeenCalled()
28 | })
29 |
30 | it('pools processes with a given id', () => {
31 | const spawned = {
32 | getId: jest.fn(),
33 | on: jest.fn()
34 | }
35 | pool.add(spawned, 11)
36 | expect(pool.processes[11]).toBe(spawned)
37 | expect(spawned.getId).not.toHaveBeenCalled()
38 | expect(spawned.on).toHaveBeenCalledTimes(1)
39 | })
40 |
41 | it('can find process in the current pool', () => {
42 | const spawned = {
43 | on: jest.fn()
44 | }
45 | pool.add(spawned, 11)
46 | expect(pool.findProcess(11)).toBe(spawned)
47 | })
48 |
49 | it('removes processes from the pool when they close', () => {
50 | const spawned = new EventEmitter()
51 |
52 | pool.add(spawned, 11)
53 | expect(pool.processes[11]).toBe(spawned)
54 |
55 | spawned.emit('close')
56 | expect(pool.processes[11]).toBe(undefined)
57 | })
58 |
59 | it('can handle removed processes on close', () => {
60 | const spawned = new EventEmitter()
61 |
62 | pool.add(spawned, 11)
63 | expect(pool.processes[11]).toBe(spawned)
64 | pool.clear()
65 | expect(pool.processes[11]).toBe(undefined)
66 |
67 | spawned.emit('close')
68 | })
69 |
--------------------------------------------------------------------------------
/src/styles/blocks/selective.scss:
--------------------------------------------------------------------------------
1 | @use "sass:color";
2 |
3 | .selective-toggle {
4 | height: 16px;
5 | margin-left: 3px;
6 | margin-right: 7px;
7 | position: relative;
8 | text-align: center;
9 | width: 14px;
10 |
11 | button {
12 | appearance: none;
13 | background-color: transparent;
14 | border: 0;
15 | border-color: var(--color-fg-subtler);
16 | border-radius: 100%;
17 | border-style: double;
18 | border-width: 4px;
19 | box-shadow: none;
20 | display: inline-block;
21 | height: 100%;
22 | height: 12px;
23 | opacity: 1;
24 | padding: 0;
25 | position: absolute;
26 | top: 2px;
27 | transition: transform 240ms cubic-bezier(.18, 1.4, .4, 1);
28 | width: 12px;
29 | z-index: 1;
30 |
31 | @include win32 {
32 | // Button and input positioning is tricky, so just override
33 | // as needed in Windows environments.
34 | top: 3px;
35 | }
36 |
37 | &:hover,
38 | &:focus,
39 | &:active {
40 | outline: 0;
41 | }
42 |
43 | &[disabled] {
44 | opacity: .5;
45 | }
46 | }
47 |
48 | &:hover,
49 | &:focus,
50 | &:active {
51 | button {
52 | &:not([disabled]) {
53 | background-color: var(--color-link);
54 | border-color: var(--primary-background-color);
55 | transform: scale(1.5);
56 | }
57 | }
58 | }
59 |
60 | input {
61 | opacity: 0;
62 | position: relative;
63 |
64 | @include win32 {
65 | // Button and input positioning is tricky, so just override
66 | // as needed in Windows environments.
67 | top: 2px;
68 | }
69 | }
70 |
71 | .selective & {
72 | button {
73 | opacity: 0;
74 | }
75 |
76 | input {
77 | opacity: 1;
78 | transition: none;
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/lib/reporters/phpunit-10/bootstrap.php:
--------------------------------------------------------------------------------
1 | registerSubscriber(new \LodeApp\PHPUnit\ExtensionHook());
38 | }
39 |
40 | // Remember actual bootstrap location in case PHPUnit boots another process
41 | // for tests running in isolation.
42 | file_put_contents(__DIR__ . DIRECTORY_SEPARATOR . 'bootstrap', $bootstrap);
43 |
44 | // Now that Lode is set up, require the original user-defined bootstrap.
45 | require_once preg_replace($pattern, '', $bootstrap);
46 |
--------------------------------------------------------------------------------
/src/lib/reporters/phpunit/src/Lode.php:
--------------------------------------------------------------------------------
1 | share(new Console);
27 | }
28 |
29 | /**
30 | * Resolve the given type from the container. Will instatiate
31 | * and set the container if it's not yet available.
32 | *
33 | * @param string $abstract
34 | * @return mixed
35 | */
36 | public static function make($abstract)
37 | {
38 | if (is_null(static::$instance)) {
39 | static::$instance = new static;
40 | }
41 |
42 | return call_user_func_array([static::$instance, 'resolve'], [$abstract]);
43 | }
44 |
45 | /**
46 | * Normalize a class name.
47 | *
48 | * @param string $abstract
49 | * @return mixed
50 | */
51 | protected function normalizeName($className)
52 | {
53 | return ltrim(strtolower($className), '\\');
54 | }
55 |
56 | /**
57 | * Share an instance through the container.
58 | *
59 | * @param string $abstract
60 | * @return mixed
61 | */
62 | protected function share($obj)
63 | {
64 | $normalizedName = $this->normalizeName(get_class($obj));
65 | $this->shares[$normalizedName] = $obj;
66 | }
67 |
68 | /**
69 | * Return th shared class from the container.
70 | *
71 | * @param string $abstract
72 | * @return mixed
73 | */
74 | protected function resolve($abstract)
75 | {
76 | $normalizedClass = $this->normalizeName($abstract);
77 | if (isset($this->shares[$normalizedClass])) {
78 | return $this->shares[$normalizedClass];
79 | }
80 |
81 | return null;
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/renderer/components/modals/About.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Lode
6 |
7 | {{ $string.set('Version :0 (:1)', version, arch) }}
8 | Release notes
9 |
10 |
11 |
12 | {{ $string.set('Electron v:0', electronVersion) }}
13 | {{ $string.set('Node v:0', nodeVersion) }}
14 |
15 |
16 |
© 2018 - :0 Tomas Buteler. All rights reserved.
17 |
18 | Terms and Conditions
19 | Open Source Notices
20 |
21 |
22 |
23 |
24 |
25 |
65 |
--------------------------------------------------------------------------------
/tests/renderer/directives/markdown.spec.js:
--------------------------------------------------------------------------------
1 | import { config, mount } from '@vue/test-utils'
2 | import Markdown from '@/directives/markdown'
3 | import Strings from '@/plugins/strings'
4 |
5 | const strings = new Strings()
6 | config.global.plugins = [strings]
7 | config.global.directives = {
8 | markdown: Markdown()
9 | }
10 |
11 | it('generates markdown text', () => {
12 | const wrapper = mount({ template: 'I like **Hobnobs** and _Digestives_!
' })
13 | expect(wrapper.html()).toBe('I like Hobnobs and Digestives !
')
14 | })
15 |
16 | it('generates markdown text from attribute', () => {
17 | const wrapper = mount({ template: '
' })
18 | expect(wrapper.html()).toBe('I like Hobnobs and Digestives !
')
19 | })
20 |
21 | it('composes strings', () => {
22 | const wrapper = mount({ template: 'I like **:0**!
' })
23 | expect(wrapper.html()).toBe('I like Hobnobs !
')
24 | })
25 |
26 | it('composes strings with arrays', () => {
27 | const wrapper = mount({ template: 'I like **:0** and _:1_!
' })
28 | expect(wrapper.html()).toBe('I like Hobnobs and Digestives !
')
29 | })
30 |
31 | it('generates block markdown', () => {
32 | const wrapper = mount({
33 | template: `
34 |
35 | # Top biscuits
36 |
`
37 | })
38 | expect(wrapper.html()).toBe(`
39 |
Top biscuits
40 | `)
41 | })
42 |
43 | it('updates if props change', async () => {
44 | const wrapper = mount({
45 | template: '**:0** are my favourite biscuits.
',
46 | props: ['favourite']
47 | }, {
48 | propsData: {
49 | favourite: 'Hobnobs'
50 | }
51 | })
52 | expect(wrapper.html()).toBe('Hobnobs are my favourite biscuits.
')
53 | await wrapper.setProps({ favourite: 'Digestives' })
54 | expect(wrapper.html()).toBe('Digestives are my favourite biscuits.
')
55 | })
56 |
--------------------------------------------------------------------------------