Add Windows save instructions UI and fix URL #296

- Add Windows instruction dialog when saving scripts for Windows.
- Fix incorrect macOS download URL given for Linux instructions.
- Refactor UI rendering, eleminating the use of `v-html` and JavaScript
  variables to hold HTML code.
This commit is contained in:
undergroundwires
2024-01-15 22:38:39 +01:00
parent e09db0f1bd
commit 756c736e21
24 changed files with 705 additions and 537 deletions

View File

@@ -1,86 +0,0 @@
import { describe, it, expect } from 'vitest';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { IInstructionsBuilderData, InstructionsBuilder, InstructionStepBuilderType } from '@/presentation/components/Code/CodeButtons/Save/Instructions/Data/InstructionsBuilder';
import { IInstructionInfo, IInstructionListStep } from '@/presentation/components/Code/CodeButtons/Save/Instructions/InstructionListData';
describe('InstructionsBuilder', () => {
describe('withStep', () => {
it('returns itself', () => {
// arrange
const expected = new InstructionsBuilder(OperatingSystem.Android);
const step = () => createMockStep();
// act
const actual = expected.withStep(step);
// assert
expect(actual).to.equal(expected);
});
});
describe('build', () => {
it('builds with given data', () => {
// arrange
const expectedData = createMockData();
const actualData = Array<IInstructionsBuilderData>();
const builder = new InstructionsBuilder(OperatingSystem.Android);
const steps: readonly InstructionStepBuilderType[] = [createMockStep(), createMockStep()]
.map((step) => (data) => {
actualData.push(data);
return step;
});
for (const step of steps) {
builder.withStep(step);
}
// act
builder.build(expectedData);
// assert
expect(actualData.every((data) => data === expectedData));
});
it('builds with every step', () => {
// arrange
const expectedSteps = [
createMockStep('first'),
createMockStep('second'),
createMockStep('third'),
];
const builder = new InstructionsBuilder(OperatingSystem.Android);
const steps: readonly InstructionStepBuilderType[] = expectedSteps.map((step) => () => step);
for (const step of steps) {
builder.withStep(step);
}
// act
const data = builder.build(createMockData());
// assert
const actualSteps = data.steps;
expect(actualSteps).to.have.members(expectedSteps);
});
it('builds with expected OS', () => {
// arrange
const expected = OperatingSystem.Linux;
const sut = new InstructionsBuilder(expected);
// act
const actual = sut.build(createMockData()).operatingSystem;
// assert
expect(true);
expect(actual).to.equal(expected);
});
});
});
function createMockData(): IInstructionsBuilderData {
return {
fileName: 'instructions-file',
};
}
function createMockStep(identifier = 'mock step'): IInstructionListStep {
return {
action: createMockInfo(`${identifier} | action`),
code: createMockInfo(`${identifier} | code`),
};
}
function createMockInfo(identifier = 'mock info'): IInstructionInfo {
return {
instruction: `${identifier} | mock instruction`,
details: `${identifier} | mock details`,
};
}

View File

@@ -1,11 +0,0 @@
import { describe } from 'vitest';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { MacOsInstructionsBuilder } from '@/presentation/components/Code/CodeButtons/Save/Instructions/Data/MacOsInstructionsBuilder';
import { runOsSpecificInstructionBuilderTests } from './OsSpecificInstructionBuilderTestRunner';
describe('MacOsInstructionsBuilder', () => {
runOsSpecificInstructionBuilderTests({
factory: () => new MacOsInstructionsBuilder(),
os: OperatingSystem.macOS,
});
});

View File

@@ -1,28 +0,0 @@
import { it, expect } from 'vitest';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { InstructionsBuilder } from '@/presentation/components/Code/CodeButtons/Save/Instructions/Data/InstructionsBuilder';
interface ITestData {
readonly factory: () => InstructionsBuilder;
readonly os: OperatingSystem;
}
export function runOsSpecificInstructionBuilderTests(data: ITestData) {
it('builds multiple steps', () => {
// arrange
const sut = data.factory();
// act
const result = sut.build({ fileName: 'test.file' });
// assert
expect(result.steps).to.have.length.greaterThan(0);
});
it(`operatingSystem return ${OperatingSystem[data.os]}`, () => {
// arrange
const expected = data.os;
const sut = data.factory();
// act
const result = sut.build({ fileName: 'test.file' });
// assert
expect(result.operatingSystem).to.equal(expected);
});
}

View File

@@ -1,30 +0,0 @@
import { describe, it, expect } from 'vitest';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { getInstructions } from '@/presentation/components/Code/CodeButtons/Save/Instructions/InstructionListDataFactory';
import { getEnumValues } from '@/application/Common/Enum';
import { InstructionsBuilder } from '@/presentation/components/Code/CodeButtons/Save/Instructions/Data/InstructionsBuilder';
import { AllSupportedOperatingSystems } from '@tests/shared/TestCases/SupportedOperatingSystems';
describe('InstructionListDataFactory', () => {
describe('getInstructions', () => {
it('returns expected if os is supported', () => {
// arrange
const fileName = 'test.file';
// act
const actualResults = AllSupportedOperatingSystems.map((os) => getInstructions(os, fileName));
// assert
expect(actualResults.every((result) => result instanceof InstructionsBuilder));
});
it('return undefined if OS is not supported', () => {
// arrange
const expected = undefined;
const fileName = 'test.file';
const unsupportedOses = getEnumValues(OperatingSystem)
.filter((value) => !AllSupportedOperatingSystems.includes(value));
// act
const actualResults = unsupportedOses.map((os) => getInstructions(os, fileName));
// assert
expect(actualResults.every((result) => result === expected));
});
});
});

View File

@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';
import { shallowMount } from '@vue/test-utils';
import CodeInstruction from '@/presentation/components/Code/CodeButtons/Save/Instructions/CodeInstruction.vue';
import CodeInstruction from '@/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/CopyableCommand.vue';
import { expectThrowsAsync } from '@tests/shared/Assertions/ExpectThrowsAsync';
import { InjectionKeys } from '@/presentation/injectionSymbols';
import { Clipboard } from '@/presentation/components/Shared/Hooks/Clipboard/Clipboard';

View File

@@ -0,0 +1,70 @@
import { shallowMount } from '@vue/test-utils';
import PlatformInstructionSteps from '@/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/PlatformInstructionSteps.vue';
import { useCollectionState } from '@/presentation/components/Shared/Hooks/UseCollectionState';
import { InjectionKeys } from '@/presentation/injectionSymbols';
import { UseCollectionStateStub } from '@tests/unit/shared/Stubs/UseCollectionStateStub';
import { AllSupportedOperatingSystems, SupportedOperatingSystem } from '@tests/shared/TestCases/SupportedOperatingSystems';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { CategoryCollectionStateStub } from '@tests/unit/shared/Stubs/CategoryCollectionStateStub';
import WindowsInstructions from '@/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/Platforms/WindowsInstructions.vue';
import MacOsInstructions from '@/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/Platforms/MacOsInstructions.vue';
import LinuxInstructions from '@/presentation/components/Code/CodeButtons/Save/RunInstructions/Steps/Platforms/LinuxInstructions.vue';
import type { Component } from 'vue';
describe('PlatformInstructionSteps', () => {
const testScenarios: Record<SupportedOperatingSystem, Component> = {
[OperatingSystem.Windows]: WindowsInstructions,
[OperatingSystem.macOS]: MacOsInstructions,
[OperatingSystem.Linux]: LinuxInstructions,
};
AllSupportedOperatingSystems.forEach((operatingSystem) => {
it(`renders the correct component for ${OperatingSystem[operatingSystem]}`, () => {
// arrange
const expectedComponent = testScenarios[operatingSystem];
const useCollectionStateStub = new UseCollectionStateStub()
.withState(new CategoryCollectionStateStub().withOs(operatingSystem));
// act
const wrapper = mountComponent({
useCollectionState: useCollectionStateStub.get(),
});
// assert
expect(wrapper.findComponent(expectedComponent).exists()).to.equal(true);
});
it(`binds the correct filename for ${OperatingSystem[operatingSystem]}`, () => {
// arrange
const expectedFilename = 'expected-file-name.bat';
const wrappedComponent = testScenarios[operatingSystem];
const useCollectionStateStub = new UseCollectionStateStub()
.withState(new CategoryCollectionStateStub().withOs(operatingSystem));
// act
const wrapper = mountComponent({
useCollectionState: useCollectionStateStub.get(),
filename: expectedFilename,
});
// assert
const componentWrapper = wrapper.findComponent(wrappedComponent);
expect(componentWrapper.props('filename')).to.equal(expectedFilename);
});
});
});
function mountComponent(options?: {
readonly useCollectionState?: ReturnType<typeof useCollectionState>;
readonly filename?: string;
}) {
return shallowMount(PlatformInstructionSteps, {
global: {
provide: {
[InjectionKeys.useCollectionState.key]:
() => options?.useCollectionState ?? new UseCollectionStateStub().get(),
},
},
props: {
filename: options?.filename === undefined ? 'privacy-test-script.bat' : options.filename,
},
});
}