diff --git a/src/presentation/assets/icons/lightbulb.svg b/src/presentation/assets/icons/lightbulb.svg
new file mode 100644
index 00000000..4a9cd6d3
--- /dev/null
+++ b/src/presentation/assets/icons/lightbulb.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/presentation/assets/icons/square-check.svg b/src/presentation/assets/icons/square-check.svg
new file mode 100644
index 00000000..8e7748a0
--- /dev/null
+++ b/src/presentation/assets/icons/square-check.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/presentation/assets/icons/triangle-exclamation.svg b/src/presentation/assets/icons/triangle-exclamation.svg
new file mode 100644
index 00000000..246e5096
--- /dev/null
+++ b/src/presentation/assets/icons/triangle-exclamation.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/presentation/assets/styles/_colors.scss b/src/presentation/assets/styles/_colors.scss
index 3b164b3f..f184ac58 100644
--- a/src/presentation/assets/styles/_colors.scss
+++ b/src/presentation/assets/styles/_colors.scss
@@ -30,6 +30,10 @@ $color-on-surface : #4d5156;
// Background | Appears behind scrollable content.
$color-background : #e6ecf4;
+$color-success : #4CAF50;
+$color-danger : #F44336;
+$color-caution : #FFC107;
+
/*
Application-specific colors:
These are tailored to the specific needs of the application and derived from the above theme colors.
diff --git a/src/presentation/components/Scripts/Menu/Selector/Rating/CircleRating.vue b/src/presentation/components/Scripts/Menu/Selector/Rating/CircleRating.vue
new file mode 100644
index 00000000..6f7603ff
--- /dev/null
+++ b/src/presentation/components/Scripts/Menu/Selector/Rating/CircleRating.vue
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/presentation/components/Scripts/Menu/Selector/Rating/RatingCircle.vue b/src/presentation/components/Scripts/Menu/Selector/Rating/RatingCircle.vue
new file mode 100644
index 00000000..8cc8c755
--- /dev/null
+++ b/src/presentation/components/Scripts/Menu/Selector/Rating/RatingCircle.vue
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
diff --git a/src/presentation/components/Scripts/Menu/Selector/SelectionTypeDocumentation.vue b/src/presentation/components/Scripts/Menu/Selector/SelectionTypeDocumentation.vue
new file mode 100644
index 00000000..02fa22a1
--- /dev/null
+++ b/src/presentation/components/Scripts/Menu/Selector/SelectionTypeDocumentation.vue
@@ -0,0 +1,141 @@
+
+
+
+ Privacy:
+
+
+
+
+
+
+ {{ recommendation }}
+
+
+
+
+ Includes:
+
+ -
+ {{ inclusionItem }}
+
+
+
+
+
+
+
+ Considerations:
+
+ -
+ {{ considerationItem }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/presentation/components/Scripts/Menu/Selector/TheSelector.vue b/src/presentation/components/Scripts/Menu/Selector/TheSelector.vue
index 051ae56c..a582d424 100644
--- a/src/presentation/components/Scripts/Menu/Selector/TheSelector.vue
+++ b/src/presentation/components/Scripts/Menu/Selector/TheSelector.vue
@@ -8,9 +8,11 @@
@click="selectType(SelectionType.None)"
/>
- Deselect all selected scripts.
-
- 💡 Good start to dive deeper into tweaks and select only what you want.
+
@@ -22,11 +24,16 @@
@click="selectType(SelectionType.Standard)"
/>
- 🛡️ Balanced for privacy and functionality.
-
- OS and applications will function normally.
-
- 💡 Recommended for everyone
+
@@ -38,11 +45,20 @@
@click="selectType(SelectionType.Strict)"
/>
- 🚫 Stronger privacy, disables risky functions that may leak your data.
-
- ⚠️ Double check to remove scripts where you would trade functionality for privacy
-
- 💡 Recommended for daily users that prefers more privacy over non-essential functions
+
@@ -54,11 +70,15 @@
@click="selectType(SelectionType.All)"
/>
- 🔒 Strongest privacy, disabling any functionality that may leak your data.
-
- 🛑 Not designed for daily users, it will break important functionalities.
-
- 💡 Only recommended for extreme use-cases like crime labs where no leak is acceptable
+
@@ -74,12 +94,14 @@ import { ICategoryCollection } from '@/domain/ICategoryCollection';
import MenuOptionList from '../MenuOptionList.vue';
import MenuOptionListItem from '../MenuOptionListItem.vue';
import { SelectionType, setCurrentSelectionType, getCurrentSelectionType } from './SelectionTypeHandler';
+import SelectionTypeDocumentation from './SelectionTypeDocumentation.vue';
export default defineComponent({
components: {
MenuOptionList,
MenuOptionListItem,
TooltipWrapper,
+ SelectionTypeDocumentation,
},
setup() {
const {
diff --git a/src/presentation/components/Shared/Icon/IconName.ts b/src/presentation/components/Shared/Icon/IconName.ts
index 3d6450ca..7e1a5b8b 100644
--- a/src/presentation/components/Shared/Icon/IconName.ts
+++ b/src/presentation/components/Shared/Icon/IconName.ts
@@ -17,6 +17,9 @@ export const IconNames = [
'file-arrow-down',
'floppy-disk',
'play',
+ 'lightbulb',
+ 'square-check',
+ 'triangle-exclamation',
] as const;
export type IconName = typeof IconNames[number];
diff --git a/tests/unit/presentation/components/Scripts/Menu/Selector/Rating/CircleRating.spec.ts b/tests/unit/presentation/components/Scripts/Menu/Selector/Rating/CircleRating.spec.ts
new file mode 100644
index 00000000..4d722bb6
--- /dev/null
+++ b/tests/unit/presentation/components/Scripts/Menu/Selector/Rating/CircleRating.spec.ts
@@ -0,0 +1,89 @@
+import { describe, it, expect } from 'vitest';
+import { shallowMount } from '@vue/test-utils';
+import CircleRating from '@/presentation/components/Scripts/Menu/Selector/Rating/CircleRating.vue';
+import RatingCircle from '@/presentation/components/Scripts/Menu/Selector/Rating/RatingCircle.vue';
+
+const MAX_RATING = 4;
+
+describe('CircleRating.vue', () => {
+ describe('number of RatingCircle components', () => {
+ it('renders correct number of RatingCircle components based on maxRating', () => {
+ // arrange
+ const expectedMaxRating = MAX_RATING;
+ const currentRating = MAX_RATING - 1;
+
+ // act
+ const wrapper = shallowMount(CircleRating, {
+ propsData: {
+ rating: currentRating,
+ },
+ });
+
+ // assert
+ const ratingCircles = wrapper.findAllComponents(RatingCircle);
+ expect(ratingCircles.length).to.equal(expectedMaxRating);
+ });
+ it('renders the correct number of RatingCircle components for default rating', () => {
+ // arrange
+ const expectedMaxRating = MAX_RATING;
+
+ // act
+ const wrapper = shallowMount(CircleRating);
+
+ // assert
+ const ratingCircles = wrapper.findAllComponents(RatingCircle);
+ expect(ratingCircles.length).to.equal(expectedMaxRating);
+ });
+ });
+
+ describe('rating logic', () => {
+ it('fills the correct number of RatingCircle components based on the provided rating', () => {
+ // arrange
+ const expectedTotalComponents = 3;
+
+ // act
+ const wrapper = shallowMount(CircleRating, {
+ propsData: {
+ rating: expectedTotalComponents,
+ },
+ });
+
+ // assert
+ const filledCircles = wrapper.findAllComponents(RatingCircle).filter((w) => w.props().filled);
+ expect(filledCircles.length).to.equal(expectedTotalComponents);
+ });
+
+ describe('validates rating correctly', () => {
+ const testCases = [
+ {
+ value: -1,
+ expectedValidationResult: false,
+ },
+ {
+ value: 0,
+ expectedValidationResult: true,
+ },
+ {
+ value: MAX_RATING - 1,
+ expectedValidationResult: true,
+ },
+ {
+ value: MAX_RATING,
+ expectedValidationResult: true,
+ },
+ ];
+ testCases.forEach((testCase) => {
+ it(`given ${testCase.value} return ${testCase.expectedValidationResult ? 'true' : 'false'}`, () => {
+ // arrange
+ const { validator } = CircleRating.props.rating;
+
+ // act
+ const actualValidationResult = validator(testCase.value);
+
+ // act
+ expect(actualValidationResult).to.equal(testCase.expectedValidationResult);
+ });
+ });
+ });
+ });
+});
diff --git a/tests/unit/presentation/components/Scripts/Menu/Selector/Rating/RatingCircle.spec.ts b/tests/unit/presentation/components/Scripts/Menu/Selector/Rating/RatingCircle.spec.ts
new file mode 100644
index 00000000..49e2c747
--- /dev/null
+++ b/tests/unit/presentation/components/Scripts/Menu/Selector/Rating/RatingCircle.spec.ts
@@ -0,0 +1,89 @@
+import { describe, it, expect } from 'vitest';
+import { shallowMount } from '@vue/test-utils';
+import RatingCircle from '@/presentation/components/Scripts/Menu/Selector/Rating/RatingCircle.vue';
+
+const DOM_SVG_SELECTOR = 'svg';
+const DOM_CIRCLE_SELECTOR = `${DOM_SVG_SELECTOR} > circle`;
+const DOM_CIRCLE_FILLED_SELECTOR = `${DOM_CIRCLE_SELECTOR}.filled`;
+
+describe('RatingCircle.vue', () => {
+ describe('circle appearance', () => {
+ it('renders a circle with the correct styles when filled', () => {
+ const wrapper = shallowMount(RatingCircle, {
+ propsData: {
+ filled: true,
+ },
+ });
+
+ const circle = wrapper.find(DOM_CIRCLE_FILLED_SELECTOR);
+ expect(circle.exists()).to.equal(true);
+ });
+
+ it('renders a circle without filled styles when not filled', () => {
+ const wrapper = shallowMount(RatingCircle, {
+ propsData: {
+ filled: false,
+ },
+ });
+
+ const circle = wrapper.find(DOM_CIRCLE_FILLED_SELECTOR);
+ expect(circle.exists()).to.equal(false);
+ });
+
+ it('renders without filled styles when filled prop is not provided', () => {
+ const wrapper = shallowMount(RatingCircle);
+
+ const circle = wrapper.find(DOM_CIRCLE_FILLED_SELECTOR);
+ expect(circle.exists()).to.equal(false);
+ });
+ });
+
+ describe('SVG and circle styles', () => {
+ it('sets --circle-stroke-width style correctly', () => {
+ const wrapper = shallowMount(RatingCircle);
+ const svgElement = wrapper.find(DOM_SVG_SELECTOR).element;
+ expect(svgElement.style.getPropertyValue('--circle-stroke-width')).to.equal('2px');
+ });
+
+ it('renders circle with correct fill attribute when filled prop is true', () => {
+ const wrapper = shallowMount(RatingCircle, {
+ propsData: {
+ filled: true,
+ },
+ });
+ const circleElement = wrapper.find(DOM_CIRCLE_FILLED_SELECTOR);
+
+ expect(circleElement.classes()).to.include('filled');
+ });
+
+ it('renders circle with the correct viewBox property', () => {
+ const wrapper = shallowMount(RatingCircle);
+ const circle = wrapper.find(DOM_SVG_SELECTOR);
+
+ expect(circle.attributes('viewBox')).to.equal('-1 -1 22 22');
+ });
+ });
+
+ describe('circle attributes', () => {
+ it('renders circle with the correct cx attribute', () => {
+ const wrapper = shallowMount(RatingCircle);
+ const circleElement = wrapper.find(DOM_CIRCLE_SELECTOR);
+
+ expect(circleElement.attributes('cx')).to.equal('10'); // Based on circleDiameterInPx = 20
+ });
+
+ it('renders circle with the correct cy attribute', () => {
+ const wrapper = shallowMount(RatingCircle);
+ const circleElement = wrapper.find(DOM_CIRCLE_SELECTOR);
+
+ expect(circleElement.attributes('cy')).to.equal('10'); // Based on circleDiameterInPx = 20
+ });
+
+ it('renders circle with the correct r attribute', () => {
+ const wrapper = shallowMount(RatingCircle);
+ const circleElement = wrapper.find(DOM_CIRCLE_SELECTOR);
+
+ expect(circleElement.attributes('r')).to.equal('9'); // Based on circleRadiusWithoutStrokeInPx = circleDiameterInPx / 2 - circleStrokeWidthInPx / 2
+ });
+ });
+});
diff --git a/tests/unit/presentation/components/Scripts/Menu/Selector/SelectionTypeDocumentation.spec.ts b/tests/unit/presentation/components/Scripts/Menu/Selector/SelectionTypeDocumentation.spec.ts
new file mode 100644
index 00000000..bb175cc4
--- /dev/null
+++ b/tests/unit/presentation/components/Scripts/Menu/Selector/SelectionTypeDocumentation.spec.ts
@@ -0,0 +1,148 @@
+import { describe, it, expect } from 'vitest';
+import { shallowMount } from '@vue/test-utils';
+import SelectionTypeDocumentation from '@/presentation/components/Scripts/Menu/Selector/SelectionTypeDocumentation.vue';
+import CircleRating from '@/presentation/components/Scripts/Menu/Selector/Rating/CircleRating.vue';
+
+const DOM_SELECTOR_INCLUDES_SECTION = '.includes';
+const DOM_SELECTOR_CONSIDERATIONS_SECTION = '.considerations';
+
+describe('SelectionTypeDocumentation.vue', () => {
+ it('renders privacy rating using CircleRating component', () => {
+ // arrange
+ const expectedPrivacyRating = 3;
+
+ // act
+ const wrapper = mountComponent({
+ privacyRating: expectedPrivacyRating,
+ });
+
+ // assert
+ const ratingComponent = wrapper.findComponent(CircleRating);
+ expect(ratingComponent.exists()).to.equal(true);
+ expect(ratingComponent.props().rating).to.equal(expectedPrivacyRating);
+ });
+
+ it('renders the provided description', () => {
+ // arrange
+ const expectedDescription = 'Some description';
+
+ // act
+ const wrapper = mountComponent({
+ description: expectedDescription,
+ });
+
+ // assert
+ expect(wrapper.text()).to.include(expectedDescription);
+ });
+
+ it('renders the provided recommendation', () => {
+ // arrange
+ const expectedRecommendation = 'Some recommendation';
+
+ // act
+ const wrapper = mountComponent({
+ recommendation: expectedRecommendation,
+ });
+
+ // assert
+ expect(wrapper.text()).to.include(expectedRecommendation);
+ });
+
+ describe('includes', () => {
+ it('renders items if provided', () => {
+ // arrange
+ const expectedIncludes = ['Item 1', 'Item 2'];
+
+ // act
+ const wrapper = mountComponent({
+ includes: expectedIncludes,
+ });
+
+ // assert
+ expect(wrapper.text()).to.include(expectedIncludes[0]);
+ expect(wrapper.text()).to.include(expectedIncludes[1]);
+ });
+
+ it('renders included section if provided', () => {
+ // arrange
+ // act
+ const wrapper = mountComponent({
+ includes: ['some', 'includes'],
+ });
+
+ // assert
+ const includesSection = wrapper.find(DOM_SELECTOR_INCLUDES_SECTION);
+ expect(includesSection.exists()).to.equal(true);
+ });
+
+ it('does not render included section if no items provided', () => {
+ // arrange
+ // act
+ const wrapper = mountComponent({
+ includes: [],
+ });
+
+ // assert
+ const includesSection = wrapper.find(DOM_SELECTOR_INCLUDES_SECTION);
+ expect(includesSection.exists()).to.equal(false);
+ });
+ });
+
+ describe('considerations', () => {
+ it('renders if provided', () => {
+ // arrange
+ const expectedConsiderations = ['Consideration 1', 'Consideration 2'];
+
+ // act
+ const wrapper = mountComponent({
+ considerations: expectedConsiderations,
+ });
+
+ // assert
+ expect(wrapper.text()).to.include(expectedConsiderations[0]);
+ expect(wrapper.text()).to.include(expectedConsiderations[1]);
+ });
+
+ it('renders included section if provided', () => {
+ // arrange
+ // act
+ const wrapper = mountComponent({
+ considerations: ['some', 'considerations'],
+ });
+
+ // assert
+ const considerationsSection = wrapper.find(DOM_SELECTOR_CONSIDERATIONS_SECTION);
+ expect(considerationsSection.exists()).to.equal(true);
+ });
+
+ it('does not render considerations section if no items provided', () => {
+ // arrange
+ // act
+ const wrapper = mountComponent({
+ considerations: [],
+ });
+
+ // assert
+ const considerationsSection = wrapper.find(DOM_SELECTOR_CONSIDERATIONS_SECTION);
+ expect(considerationsSection.exists()).to.equal(false);
+ });
+ });
+});
+
+function mountComponent(options: {
+ readonly privacyRating?: number,
+ readonly description?: string,
+ readonly recommendation?: string,
+ readonly includes?: string[],
+ readonly considerations?: string[],
+}) {
+ return shallowMount(SelectionTypeDocumentation, {
+ propsData: {
+ privacyRating: options.privacyRating ?? 0,
+ description: options.description ?? 'test-description',
+ recommendation: options.recommendation ?? 'test-recommendation',
+ considerations: options.considerations ?? [],
+ includes: options.includes ?? [],
+ },
+ });
+}