Add initial Linux support #150
Key features of Linux support: - It supports python 3 scripts execution. - It supports Flatpak and Snap installation for software clean-up/configurations. - Extensive documentation.
This commit is contained in:
@@ -21,8 +21,9 @@ A clear and concise description of what the bug is.
|
|||||||
|
|
||||||
<!--
|
<!--
|
||||||
Which OS are you using? What version of OS you were using?
|
Which OS are you using? What version of OS you were using?
|
||||||
On Windows you can find it using "Start button" > "Settings" > "System" > "About".
|
On Windows: Open "Start button" > "Settings" > "System" > "About".
|
||||||
On macOS you can find it using "Apple menu (top left corner)" > "About This Mac".
|
On macOS: Open "Apple menu (top left corner)" > "About This Mac".
|
||||||
|
On Linux: Open terminal > type: lsb_release -a > copy paste the result.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Reproduction steps
|
### Reproduction steps
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ You could alternatively send a PR directly (see CONTRIBUTING.md).
|
|||||||
|
|
||||||
<!--
|
<!--
|
||||||
Which OS will the new script configure?
|
Which OS will the new script configure?
|
||||||
Either "Windows" or "macOS".
|
One of the supported OSes: "Windows", "macOS" or "Linux".
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Name
|
### Name
|
||||||
@@ -31,9 +31,11 @@ E.g. "Disable webcam telemetry"
|
|||||||
Code that will be executed when script is selected.
|
Code that will be executed when script is selected.
|
||||||
Try to keep it as simple and backwards-compatible as possible.
|
Try to keep it as simple and backwards-compatible as possible.
|
||||||
Allowed languages:
|
Allowed languages:
|
||||||
- macOS: bash (sh)
|
|
||||||
- Windows: PowerShell (ps1) or batchfile
|
- Windows: PowerShell (ps1) or batchfile
|
||||||
- 💡 Prioritize the one that's simpler, batchfile if similar.
|
- 💡 Prioritize the one that's simpler, batchfile if similar.
|
||||||
|
- macOS: bash (sh)
|
||||||
|
- Linux: bash (sh) or Python 3
|
||||||
|
- 💡 Prioritize the one that's simpler, bash if similar.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
### Revert code
|
### Revert code
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# privacy.sexy
|
# privacy.sexy
|
||||||
|
|
||||||
> Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆
|
> Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆
|
||||||
|
|
||||||
<!-- markdownlint-disable MD033 -->
|
<!-- markdownlint-disable MD033 -->
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -120,7 +120,7 @@ Online version does not require to run any software on your computer. Offline ve
|
|||||||
- **Reversible**. Revert if something feels wrong.
|
- **Reversible**. Revert if something feels wrong.
|
||||||
- **Accessible**. No need to run any compiled software on your computer with web version.
|
- **Accessible**. No need to run any compiled software on your computer with web version.
|
||||||
- **Open**. What you see as code in this repository is what you get. The application itself, its infrastructure and deployments are open-source and automated thanks to [bump-everywhere](https://github.com/undergroundwires/bump-everywhere).
|
- **Open**. What you see as code in this repository is what you get. The application itself, its infrastructure and deployments are open-source and automated thanks to [bump-everywhere](https://github.com/undergroundwires/bump-everywhere).
|
||||||
- **Tested.** A lot of tests. Automated and manual. Community-testing and verification. Stability improvements comes before new features.
|
- **Tested**. A lot of tests. Automated and manual. Community-testing and verification. Stability improvements comes before new features.
|
||||||
- **Extensible**. Effortlessly [extend scripts](./CONTRIBUTING.md#extend-scripts) with a custom designed [templating language](./docs/templating.md).
|
- **Extensible**. Effortlessly [extend scripts](./CONTRIBUTING.md#extend-scripts) with a custom designed [templating language](./docs/templating.md).
|
||||||
- **Portable and simple**. Every script is independently executable without cross-dependencies.
|
- **Portable and simple**. Every script is independently executable without cross-dependencies.
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.11.4",
|
"version": "0.11.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"description": "Enforce privacy & security best-practices on Windows and macOS, because privacy is sexy 🍑🍆",
|
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆",
|
||||||
"author": "undergroundwires",
|
"author": "undergroundwires",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"serve": "vue-cli-service serve",
|
"serve": "vue-cli-service serve",
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||||
<title>Privacy is sexy 🍑🍆 - Enforce privacy & security on Windows and macOS</title>
|
<title>Privacy is sexy 🍑🍆 - Enforce privacy & security on Windows, macOS and Linux</title>
|
||||||
<meta name="robots" content="index,follow" />
|
<meta name="robots" content="index,follow" />
|
||||||
<meta name="description" content="Web tool to generate scripts for enforcing privacy & security best-practices such as stopping data collection of Windows and different softwares on it."/>
|
<meta name="description" content="Web tool to generate scripts for enforcing privacy & security best-practices such as stopping data collection of Windows and different softwares on it."/>
|
||||||
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { IProjectInformation } from '@/domain/IProjectInformation';
|
|||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import WindowsData from '@/application/collections/windows.yaml';
|
import WindowsData from '@/application/collections/windows.yaml';
|
||||||
import MacOsData from '@/application/collections/macos.yaml';
|
import MacOsData from '@/application/collections/macos.yaml';
|
||||||
|
import LinuxData from '@/application/collections/linux.yaml';
|
||||||
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
|
||||||
import { Application } from '@/domain/Application';
|
import { Application } from '@/domain/Application';
|
||||||
import { parseCategoryCollection } from './CategoryCollectionParser';
|
import { parseCategoryCollection } from './CategoryCollectionParser';
|
||||||
@@ -28,7 +29,7 @@ const CategoryCollectionParser: CategoryCollectionParserType = (file, info) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const PreParsedCollections: readonly CollectionData [] = [
|
const PreParsedCollections: readonly CollectionData [] = [
|
||||||
WindowsData, MacOsData,
|
WindowsData, MacOsData, LinuxData,
|
||||||
];
|
];
|
||||||
|
|
||||||
function validateCollectionsData(collections: readonly CollectionData[]) {
|
function validateCollectionsData(collections: readonly CollectionData[]) {
|
||||||
|
|||||||
@@ -3,5 +3,5 @@ import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/I
|
|||||||
export class ShellScriptSyntax implements ILanguageSyntax {
|
export class ShellScriptSyntax implements ILanguageSyntax {
|
||||||
public readonly commentDelimiters = ['#'];
|
public readonly commentDelimiters = ['#'];
|
||||||
|
|
||||||
public readonly commonCodeParts = ['(', ')', 'else', 'fi'];
|
public readonly commonCodeParts = ['(', ')', 'else', 'fi', 'done'];
|
||||||
}
|
}
|
||||||
|
|||||||
3690
src/application/collections/linux.yaml
Normal file
3690
src/application/collections/linux.yaml
Normal file
File diff suppressed because it is too large
Load Diff
@@ -26,6 +26,8 @@ export class CodeRunner {
|
|||||||
|
|
||||||
function getExecuteCommand(scriptPath: string, environment: Environment): string {
|
function getExecuteCommand(scriptPath: string, environment: Environment): string {
|
||||||
switch (environment.os) {
|
switch (environment.os) {
|
||||||
|
case OperatingSystem.Linux:
|
||||||
|
return `x-terminal-emulator -e '${scriptPath}'`;
|
||||||
case OperatingSystem.macOS:
|
case OperatingSystem.macOS:
|
||||||
return `open -a Terminal.app ${scriptPath}`;
|
return `open -a Terminal.app ${scriptPath}`;
|
||||||
// Another option with graphical sudo would be
|
// Another option with graphical sudo would be
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import { InstructionsBuilder } from './InstructionsBuilder';
|
||||||
|
|
||||||
|
export class LinuxInstructionsBuilder extends InstructionsBuilder {
|
||||||
|
constructor() {
|
||||||
|
super(OperatingSystem.Linux);
|
||||||
|
super
|
||||||
|
.withStep(() => ({
|
||||||
|
action: {
|
||||||
|
instruction: 'Download the file.',
|
||||||
|
details: 'You should have already been prompted to save the script file.'
|
||||||
|
+ '<br/>If this was not the case or you did not save the script when prompted,'
|
||||||
|
+ '<br/>please try to download your script file again.',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.withStep(() => ({
|
||||||
|
action: {
|
||||||
|
instruction: 'Open terminal.',
|
||||||
|
details:
|
||||||
|
'Opening terminal changes based on the distro you run.'
|
||||||
|
+ '<br/>You may search for "Terminal" in your application launcher to find it.'
|
||||||
|
+ '<br/>'
|
||||||
|
+ '<br/>Alternatively use terminal shortcut for your distro if it has one by default:'
|
||||||
|
+ '<ul>'
|
||||||
|
+ '<li><code>Ctrl-Alt-T</code>: Ubuntu, CentOS, Linux Mint, Elementary OS, ubermix, Kali…</li>'
|
||||||
|
+ '<li><code>Super-T</code>: Pop!_OS…</li>'
|
||||||
|
+ '<li><code>Alt-T</code>: Parrot OS…</li>'
|
||||||
|
+ '<li><code>Ctrl-Alt-Insert</code>: Bodhi Linux…</li>'
|
||||||
|
+ '</ul>'
|
||||||
|
+ '<br/>'
|
||||||
|
,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.withStep(() => ({
|
||||||
|
action: {
|
||||||
|
instruction: 'Navigate to the folder where you downloaded the file e.g.:',
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
instruction: 'cd ~/Downloads',
|
||||||
|
details: 'Press on <code>enter/return</code> key after running the command.'
|
||||||
|
+ '<br/>If the file is not downloaded on Downloads folder,'
|
||||||
|
+ '<br/>change <code>Downloads</code> to path where the file is downloaded.'
|
||||||
|
+ '<br/>'
|
||||||
|
+ '<br/>This command means:'
|
||||||
|
+ '<ul>'
|
||||||
|
+ '<li><code>cd</code> will change the current folder.</li>'
|
||||||
|
+ '<li><code>~</code> is the user home directory.</li>'
|
||||||
|
+ '</ul>',
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.withStep((data) => ({
|
||||||
|
action: {
|
||||||
|
instruction: 'Give the file execute permissions:',
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
instruction: `chmod +x ${data.fileName}`,
|
||||||
|
details: 'Press on <code>enter/return</code> key after running the command.<br/>'
|
||||||
|
+ 'It will make the file executable. <br/>'
|
||||||
|
+ 'If you use desktop environment you can alternatively (instead of running the command):'
|
||||||
|
+ '<ol>'
|
||||||
|
+ '<li>Locate the file using your file manager.</li>'
|
||||||
|
+ '<li>Right click on the file, select "Properties".</li>'
|
||||||
|
+ '<li>Go to "Permissions" and check "Allow executing file as program".</li>'
|
||||||
|
+ '</ol>'
|
||||||
|
+ '<br/>These GUI steps and name of options may change depending on your file manager.'
|
||||||
|
,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.withStep((data) => ({
|
||||||
|
action: {
|
||||||
|
instruction: 'Execute the file:',
|
||||||
|
},
|
||||||
|
code: {
|
||||||
|
instruction: `./${data.fileName}`,
|
||||||
|
details:
|
||||||
|
'If you have desktop environment, instead of running this command you can alternatively:'
|
||||||
|
+ '<ol>'
|
||||||
|
+ '<li>Locate the file using your file manager.</li>'
|
||||||
|
+ '<li>Right click on the file, select "Run as program".</li>'
|
||||||
|
+ '</ol>'
|
||||||
|
,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
.withStep(() => ({
|
||||||
|
action: {
|
||||||
|
instruction: 'If asked, enter your administrator password.',
|
||||||
|
details: 'As you type, your password will be hidden but the keys are still registered, so keep typing.'
|
||||||
|
+ '<br/>Press on <code>enter/return</code> key after typing your password.'
|
||||||
|
+ '<br/>Administrator privileges are required to configure OS.',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,11 @@ import { OperatingSystem } from '@/domain/OperatingSystem';
|
|||||||
import { InstructionsBuilder } from './Data/InstructionsBuilder';
|
import { InstructionsBuilder } from './Data/InstructionsBuilder';
|
||||||
import { MacOsInstructionsBuilder } from './Data/MacOsInstructionsBuilder';
|
import { MacOsInstructionsBuilder } from './Data/MacOsInstructionsBuilder';
|
||||||
import { IInstructionListData } from './InstructionListData';
|
import { IInstructionListData } from './InstructionListData';
|
||||||
|
import { LinuxInstructionsBuilder } from './Data/LinuxInstructionsBuilder';
|
||||||
|
|
||||||
const builders = new Map<OperatingSystem, InstructionsBuilder>([
|
const builders = new Map<OperatingSystem, InstructionsBuilder>([
|
||||||
[OperatingSystem.macOS, new MacOsInstructionsBuilder()],
|
[OperatingSystem.macOS, new MacOsInstructionsBuilder()],
|
||||||
|
[OperatingSystem.Linux, new LinuxInstructionsBuilder()],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export function hasInstructions(os: OperatingSystem) {
|
export function hasInstructions(os: OperatingSystem) {
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ function renderOsName(os: OperatingSystem): string {
|
|||||||
switch (os) {
|
switch (os) {
|
||||||
case OperatingSystem.Windows: return 'Windows';
|
case OperatingSystem.Windows: return 'Windows';
|
||||||
case OperatingSystem.macOS: return 'macOS';
|
case OperatingSystem.macOS: return 'macOS';
|
||||||
|
case OperatingSystem.Linux: return 'Linux (preview)';
|
||||||
default: throw new RangeError(`Cannot render os name: ${OperatingSystem[os]}`);
|
default: throw new RangeError(`Cannot render os name: ${OperatingSystem[os]}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ function beatifyAutoLinks(content: string): string {
|
|||||||
if (!content) {
|
if (!content) {
|
||||||
return content;
|
return content;
|
||||||
}
|
}
|
||||||
return content.replaceAll(/(?<!\]\(|\[\d+\]:\s+|https?\S+)((?:https?):\/\/[^\s\])]*)(?:[\])](?!\()|$|\s)/gm, (_$, urlMatch) => {
|
return content.replaceAll(/(?<!\]\(|\[\d+\]:\s+|https?\S+|`)((?:https?):\/\/[^\s\])]*)(?:[\])](?!\()|$|\s)/gm, (_$, urlMatch) => {
|
||||||
return toReadableLink(urlMatch);
|
return toReadableLink(urlMatch);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ function hasDesktopVersion(os: OperatingSystem): boolean {
|
|||||||
function getOperatingSystemName(os: OperatingSystem): string {
|
function getOperatingSystemName(os: OperatingSystem): string {
|
||||||
switch (os) {
|
switch (os) {
|
||||||
case OperatingSystem.Linux:
|
case OperatingSystem.Linux:
|
||||||
return 'Linux';
|
return 'Linux (preview)';
|
||||||
case OperatingSystem.macOS:
|
case OperatingSystem.macOS:
|
||||||
return 'macOS';
|
return 'macOS';
|
||||||
case OperatingSystem.Windows:
|
case OperatingSystem.Windows:
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="container">
|
<div id="container">
|
||||||
<h1 class="child title" >{{ title }}</h1>
|
<h1 class="child title" >{{ title }}</h1>
|
||||||
<h2 class="child subtitle">Enforce privacy & security on Windows and macOS</h2>
|
<h2 class="child subtitle">Enforce privacy & security on Windows, macOS and Linux</h2>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|||||||
@@ -33,17 +33,18 @@ describe('collections', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function collectUniqueUrls(app: IApplication): string[] {
|
function collectUniqueUrls(app: IApplication): string[] {
|
||||||
return app
|
return [ // Get all nodes
|
||||||
.collections
|
...app.collections.flatMap((c) => c.getAllCategories()),
|
||||||
.flatMap((a) => a.getAllScripts())
|
...app.collections.flatMap((c) => c.getAllScripts()),
|
||||||
.flatMap((script) => script.docs?.flatMap((doc) => parseUrls(doc)))
|
]
|
||||||
|
// Get all docs
|
||||||
|
.flatMap((documentable) => documentable.docs)
|
||||||
|
// Parse all URLs
|
||||||
|
.flatMap((docString) => docString.match(/(https?:\/\/[^\s]+)/g) || [])
|
||||||
|
// Remove duplicates
|
||||||
.filter((url, index, array) => array.indexOf(url) === index);
|
.filter((url, index, array) => array.indexOf(url) === index);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseUrls(text: string): string[] {
|
|
||||||
return text?.match(/\bhttps?:\/\/\S+/gi) ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function printUrls(statuses: IUrlStatus[]): string {
|
function printUrls(statuses: IUrlStatus[]): string {
|
||||||
/* eslint-disable prefer-template */
|
/* eslint-disable prefer-template */
|
||||||
return '\n'
|
return '\n'
|
||||||
|
|||||||
@@ -12,16 +12,17 @@ describe('MarkdownRenderer', () => {
|
|||||||
it(`${node.nodeLabel}`, () => {
|
it(`${node.nodeLabel}`, () => {
|
||||||
// act
|
// act
|
||||||
const html = renderer.render(node.docs);
|
const html = renderer.render(node.docs);
|
||||||
|
const result = validateHtml(html);
|
||||||
// assert
|
// assert
|
||||||
expect(isValidHtml(html));
|
expect(result.isValid, result.generatedHtml);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
interface IDocumentableNode {
|
interface IDocumentableNode {
|
||||||
nodeLabel: string
|
readonly nodeLabel: string
|
||||||
docs: string
|
readonly docs: string
|
||||||
}
|
}
|
||||||
function* collectAllDocumentableNodes(): Generator<IDocumentableNode> {
|
function* collectAllDocumentableNodes(): Generator<IDocumentableNode> {
|
||||||
const app = parseApplication();
|
const app = parseApplication();
|
||||||
@@ -40,7 +41,16 @@ function* collectAllDocumentableNodes(): Generator<IDocumentableNode> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isValidHtml(value: string): boolean {
|
interface IHTMLValidationResult {
|
||||||
const doc = new window.DOMParser().parseFromString(value, 'text/html');
|
readonly isValid: boolean;
|
||||||
return Array.from(doc.body.childNodes).some((node) => node.nodeType === 1);
|
readonly generatedHtml: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateHtml(value: string): IHTMLValidationResult {
|
||||||
|
const doc = new window.DOMParser()
|
||||||
|
.parseFromString(value, 'text/html');
|
||||||
|
return {
|
||||||
|
isValid: Array.from(doc.body.childNodes).some((node) => node.nodeType === 1),
|
||||||
|
generatedHtml: doc.body.innerHTML,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { parseProjectInformation } from '@/application/Parser/ProjectInformation
|
|||||||
import { CategoryCollectionParserType, parseApplication } from '@/application/Parser/ApplicationParser';
|
import { CategoryCollectionParserType, parseApplication } from '@/application/Parser/ApplicationParser';
|
||||||
import WindowsData from '@/application/collections/windows.yaml';
|
import WindowsData from '@/application/collections/windows.yaml';
|
||||||
import MacOsData from '@/application/collections/macos.yaml';
|
import MacOsData from '@/application/collections/macos.yaml';
|
||||||
|
import LinuxData from '@/application/collections/linux.yaml';
|
||||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||||
import { ProjectInformation } from '@/domain/ProjectInformation';
|
import { ProjectInformation } from '@/domain/ProjectInformation';
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
@@ -101,7 +102,7 @@ describe('ApplicationParser', () => {
|
|||||||
});
|
});
|
||||||
it('defaults to expected data', () => {
|
it('defaults to expected data', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expected = [WindowsData, MacOsData];
|
const expected = [WindowsData, MacOsData, LinuxData];
|
||||||
const parserSpy = new CategoryCollectionParserSpy();
|
const parserSpy = new CategoryCollectionParserSpy();
|
||||||
const parserMock = parserSpy.mockParser();
|
const parserMock = parserSpy.mockParser();
|
||||||
// act
|
// act
|
||||||
|
|||||||
@@ -77,13 +77,20 @@ describe('CodeRunner', () => {
|
|||||||
describe('executes as expected', () => {
|
describe('executes as expected', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const filePath = 'expected-file-path';
|
const filePath = 'expected-file-path';
|
||||||
const testData = [{
|
const testData = [
|
||||||
os: OperatingSystem.Windows,
|
{
|
||||||
expected: filePath,
|
os: OperatingSystem.Windows,
|
||||||
}, {
|
expected: filePath,
|
||||||
os: OperatingSystem.macOS,
|
},
|
||||||
expected: `open -a Terminal.app ${filePath}`,
|
{
|
||||||
}];
|
os: OperatingSystem.macOS,
|
||||||
|
expected: `open -a Terminal.app ${filePath}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
os: OperatingSystem.Linux,
|
||||||
|
expected: `x-terminal-emulator -e '${filePath}'`,
|
||||||
|
},
|
||||||
|
];
|
||||||
for (const data of testData) {
|
for (const data of testData) {
|
||||||
it(`returns ${data.expected} on ${OperatingSystem[data.os]}`, async () => {
|
it(`returns ${data.expected} on ${OperatingSystem[data.os]}`, async () => {
|
||||||
const context = new TestContext();
|
const context = new TestContext();
|
||||||
|
|||||||
Reference in New Issue
Block a user