diff --git a/misc/swift-file-samples/Enzo.swift b/misc/swift-file-samples/Enzo.swift new file mode 100644 index 0000000..ed54603 --- /dev/null +++ b/misc/swift-file-samples/Enzo.swift @@ -0,0 +1,133 @@ +/* +"DecorativeView.swift" file from the iOS SwiftUI Accessibility Techniques +project from CVS Health, modified to be used in the tests. +*/ + +/* + Copyright 2023 CVS Health and/or one of its affiliates + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import SwiftUI + +struct DecorativeView: View { + + private var darkGreen = Color(red: 0 / 255, green: 102 / 255, blue: 0 / 255) + private var darkRed = Color(red: 220 / 255, green: 20 / 255, blue: 60 / 255) + @Environment(\.colorScheme) var colorScheme + + var body: some View { + ScrollView { + VStack { + Text("Decorative images are used purely for decoration and convey no meaning to sighted users. Decorative images must be hidden from VoiceOver users. Use `Image(decorative:)` or `.accessibilityHidden(true)` to hide decorative images from VoiceOver.") + .padding(.bottom) + Text("Good Examples") + .font(.subheadline) + .fontWeight(.bold) + .frame(maxWidth: .infinity, alignment: .leading) + .accessibilityAddTraits(.isHeader) + .foregroundColor(colorScheme == .dark ? Color(.systemGreen) : darkGreen) + Divider() + .frame(height: 2.0, alignment:.leading) + .background(colorScheme == .dark ? Color(.systemGreen) : darkGreen) + .padding(.bottom) + Text("Good Example `Image(decorative:)`") + .font(.subheadline) + .fontWeight(.bold) + .frame(maxWidth: .infinity, alignment: .leading) + .accessibilityAddTraits(.isHeader) + Image(decorative: "newspaper") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 100, height: 100) + .accessibilityIdentifier("goodImage") + Text("Discover new offers every week and earn extra savings.") + Link(destination: URL(string: "https://www.example.com/weeklyad")!) { + Text("Shop weekly ad") + .underline() + .padding(.bottom, 10) + } + DisclosureGroup("Details") { + Text("The good decorative image example uses `Image(decorative: \"newspaper\")` which prevents VoiceOver from focusing on the image.") + }.padding(.bottom).accessibilityHint("Good Example Image(decorative:)") + Text("Good Example `.accessibilityHidden(true)`") + .font(.subheadline) + .fontWeight(.bold) + .frame(maxWidth: .infinity, alignment: .leading) + .accessibilityAddTraits(.isHeader) + Image(systemName: "globe") + .imageScale(.large) + .foregroundColor(.accentColor) + .accessibilityHidden(true) + .accessibilityIdentifier("goodIcon") + Text("Hello, world!") + DisclosureGroup("Details") { + Text("The good decorative icon image example uses `.accessibilityHidden(true)` which prevents VoiceOver from focusing on the icon.") + }.padding(.bottom).accessibilityHint("Good Example `.accessibilityHidden(true)`") + Text("Bad Examples") + .font(.subheadline) + .fontWeight(.bold) + .frame(maxWidth: .infinity, alignment: .leading) + .accessibilityAddTraits(.isHeader) + .foregroundColor(colorScheme == .dark ? Color(.systemRed) : darkRed) + Divider() + .frame(height: 2.0, alignment:.leading) + .background(colorScheme == .dark ? Color(.systemRed) : darkRed) + .padding(.bottom) + Text("Bad Example Missing `Image(decorative:)`") + .font(.subheadline) + .fontWeight(.bold) + .frame(maxWidth: .infinity, alignment: .leading) + .accessibilityAddTraits(.isHeader) + Image("newspaper") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 100, height: 100) + .accessibilityIdentifier("badImage") + Text("Discover new offers every week and earn extra savings.") + Link(destination: URL(string: "https://www.example.com/weeklyad")!) { + Text("Shop weekly ad") + .underline() + .padding(.bottom, 10) + } + DisclosureGroup("Details") { + Text("The bad decorative image example does not use the `decorative:` parameter which allows VoiceOver to focus on the image and read \"newspaper\" as its accessibility label.") + }.padding(.bottom).accessibilityHint("Bad Example Missing `Image(decorative:)`") + Text("Bad Example Missing `.accessibilityHidden(true)`") + .font(.subheadline) + .fontWeight(.bold) + .frame(maxWidth: .infinity, alignment: .leading) + .accessibilityAddTraits(.isHeader) + Image(systemName: "globe") + .imageScale(.large) + .foregroundColor(.accentColor) + .accessibilityIdentifier("badIcon") + Text("Hello, world!") + DisclosureGroup("Details") { + Text("The bad decorative icon image example does not use `.accessibilityHidden(true)` which allows VoiceOver to focus on the image and read \"globe\" as its accessibility label.") + }.padding(.bottom).accessibilityHint("Bad Example Missing `.accessibilityHidden(true)`") + } + .navigationTitle("Decorative Images") + .padding() + } + } +} + +struct DecorativeView_Previews: PreviewProvider { + static var previews: some View { + NavigationStack { + DecorativeView() + } + } +} diff --git a/misc/swift-file-samples/Enzo_noImageDecorativeLabel.swift b/misc/swift-file-samples/Enzo_noImageDecorativeLabel.swift new file mode 100644 index 0000000..da61fbe --- /dev/null +++ b/misc/swift-file-samples/Enzo_noImageDecorativeLabel.swift @@ -0,0 +1,133 @@ +/* +"DecorativeView.swift" file from the iOS SwiftUI Accessibility Techniques +project from CVS Health, modified to be used in the tests. +*/ + +/* + Copyright 2023 CVS Health and/or one of its affiliates + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + */ + +import SwiftUI + +struct DecorativeView: View { + + private var darkGreen = Color(red: 0 / 255, green: 102 / 255, blue: 0 / 255) + private var darkRed = Color(red: 220 / 255, green: 20 / 255, blue: 60 / 255) + @Environment(\.colorScheme) var colorScheme + + var body: some View { + ScrollView { + VStack { + Text("Decorative images are used purely for decoration and convey no meaning to sighted users. Decorative images must be hidden from VoiceOver users. Use `Image(decorative:)` or `.accessibilityHidden(true)` to hide decorative images from VoiceOver.") + .padding(.bottom) + Text("Good Examples") + .font(.subheadline) + .fontWeight(.bold) + .frame(maxWidth: .infinity, alignment: .leading) + .accessibilityAddTraits(.isHeader) + .foregroundColor(colorScheme == .dark ? Color(.systemGreen) : darkGreen) + Divider() + .frame(height: 2.0, alignment:.leading) + .background(colorScheme == .dark ? Color(.systemGreen) : darkGreen) + .padding(.bottom) + Text("Good Example `Image(decorative:)`") + .font(.subheadline) + .fontWeight(.bold) + .frame(maxWidth: .infinity, alignment: .leading) + .accessibilityAddTraits(.isHeader) + Image("newspaper") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 100, height: 100) + .accessibilityIdentifier("goodImage") + Text("Discover new offers every week and earn extra savings.") + Link(destination: URL(string: "https://www.example.com/weeklyad")!) { + Text("Shop weekly ad") + .underline() + .padding(.bottom, 10) + } + DisclosureGroup("Details") { + Text("The good decorative image example uses `Image(decorative: \"newspaper\")` which prevents VoiceOver from focusing on the image.") + }.padding(.bottom).accessibilityHint("Good Example Image(decorative:)") + Text("Good Example `.accessibilityHidden(true)`") + .font(.subheadline) + .fontWeight(.bold) + .frame(maxWidth: .infinity, alignment: .leading) + .accessibilityAddTraits(.isHeader) + Image(systemName: "globe") + .imageScale(.large) + .foregroundColor(.accentColor) + .accessibilityHidden(true) + .accessibilityIdentifier("goodIcon") + Text("Hello, world!") + DisclosureGroup("Details") { + Text("The good decorative icon image example uses `.accessibilityHidden(true)` which prevents VoiceOver from focusing on the icon.") + }.padding(.bottom).accessibilityHint("Good Example `.accessibilityHidden(true)`") + Text("Bad Examples") + .font(.subheadline) + .fontWeight(.bold) + .frame(maxWidth: .infinity, alignment: .leading) + .accessibilityAddTraits(.isHeader) + .foregroundColor(colorScheme == .dark ? Color(.systemRed) : darkRed) + Divider() + .frame(height: 2.0, alignment:.leading) + .background(colorScheme == .dark ? Color(.systemRed) : darkRed) + .padding(.bottom) + Text("Bad Example Missing `Image(decorative:)`") + .font(.subheadline) + .fontWeight(.bold) + .frame(maxWidth: .infinity, alignment: .leading) + .accessibilityAddTraits(.isHeader) + Image("newspaper") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 100, height: 100) + .accessibilityIdentifier("badImage") + Text("Discover new offers every week and earn extra savings.") + Link(destination: URL(string: "https://www.example.com/weeklyad")!) { + Text("Shop weekly ad") + .underline() + .padding(.bottom, 10) + } + DisclosureGroup("Details") { + Text("The bad decorative image example does not use the `decorative:` parameter which allows VoiceOver to focus on the image and read \"newspaper\" as its accessibility label.") + }.padding(.bottom).accessibilityHint("Bad Example Missing `Image(decorative:)`") + Text("Bad Example Missing `.accessibilityHidden(true)`") + .font(.subheadline) + .fontWeight(.bold) + .frame(maxWidth: .infinity, alignment: .leading) + .accessibilityAddTraits(.isHeader) + Image(systemName: "globe") + .imageScale(.large) + .foregroundColor(.accentColor) + .accessibilityIdentifier("badIcon") + Text("Hello, world!") + DisclosureGroup("Details") { + Text("The bad decorative icon image example does not use `.accessibilityHidden(true)` which allows VoiceOver to focus on the image and read \"globe\" as its accessibility label.") + }.padding(.bottom).accessibilityHint("Bad Example Missing `.accessibilityHidden(true)`") + } + .navigationTitle("Decorative Images") + .padding() + } + } +} + +struct DecorativeView_Previews: PreviewProvider { + static var previews: some View { + NavigationStack { + DecorativeView() + } + } +} diff --git a/src/deaccessibilizer.ts b/src/deaccessibilizer.ts index 58e6de5..1a8c842 100644 --- a/src/deaccessibilizer.ts +++ b/src/deaccessibilizer.ts @@ -1,32 +1,123 @@ import Parser, { QueryMatch } from 'web-tree-sitter'; import { SwiftFileTree } from './parsing/swift-file-tree.js'; import { getParser } from './configuring/get-parser.js'; -import { FaultTransformationRule } from './transforming/fault-transformation-rule.js'; +import { + FaultTransformationOptions, + FaultTransformationRule, +} from './transforming/fault-transformation-rule.js'; +import { RulesDictionary } from './transforming/rules-dictionary.js'; +import { CodeTransformation } from './transforming/code-transformation.js'; +import { byNodePosition } from './utils.js'; + +const allRules = Object.values(RulesDictionary); /** - * The main class of the library, responsible for handling Swift files, creating syntax trees, performing queries, and transforming code. + * The main class of the library, responsible for handling Swift files, creating syntax trees, + * performing queries, and transforming code. * * @category Main */ export class Deaccessibilizer { private parser: Promise; + /** + * Create a new instance of the Deaccessibilizer. Gets the tree-sitter parser for Swift, + * considering whether it is running in Node.js or WebAssembly. + */ constructor() { this.parser = getParser() as unknown as Promise; } + /** + * Create a Swift file tree from the given file text. + */ async createSwiftFileTree(fileText: string): Promise { const parser = await this.parser; return new SwiftFileTree(fileText, parser); } - async queryFromRule(tree: SwiftFileTree, rule: FaultTransformationRule) { - const matches: QueryMatch[] = []; + /** + * Query a Swift file tree with a [query in tree-sitter syntax](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#queries). + */ + queryEntireTree(tree: SwiftFileTree, query: string): QueryMatch[] { + return tree.queryNode(tree.tree.rootNode, query); + } - for (const view of tree.swiftUIViews) { - matches.push(...tree.queryNode(view, rule.queryText)); + /** + * Query the SwiftUI views in the file tree with a [query in tree-sitter syntax](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#queries). + */ + querySwiftUIViews(tree: SwiftFileTree, query: string): QueryMatch[] { + return tree.swiftUIViews.flatMap((view) => tree.queryNode(view, query)); + } + + /** + * Get the code transformations introducing a fault that can be applied to the given tree, + * based on the given rules. If no rules are provided, all rules are considered. + */ + getFaultTransformations( + tree: SwiftFileTree, + rules: FaultTransformationRule[] = allRules, + options: FaultTransformationOptions = { + substituteWithComment: false, + }, + ): CodeTransformation[] { + return rules.flatMap((rule) => + this.querySwiftUIViews(tree, rule.queryText).map((match) => + rule.getFaultTransformation(match, options), + ), + ); + } + + /** + * Apply the given code transformations to the tree. + * + * This method is faster than {@link applyFaultTransformationsToTreeWithRebuild}, + * but it is less safe, since it sorts and applies all transformations at once. + * + * This is reccomended for small batches of code transformations. + */ + applyCodeTransformationsToTree( + tree: SwiftFileTree, + codeTransformations: CodeTransformation[], + ) { + const nodeChanges = codeTransformations + .flatMap((transformation) => transformation.nodeChanges) + .sort(byNodePosition) + .reverse(); + + for (const nodeChange of nodeChanges) { + tree.replaceNode( + nodeChange.node, + nodeChange.replaceWith, + nodeChange.replaceOptions, + ); } + } - return matches; + /** + * Get the fault transformations from the given rules and apply them directly to the tree. + * If no rules are provided, all rules are considered. + * + * This method is safer than {@link applyCodeTransformationsToTree}, since it applies + * one rule at a time and rebuilds the tree after each transformation. + * + * This is reccomended for large batches of code transformations. + */ + applyFaultTransformationsToTreeWithRebuild( + tree: SwiftFileTree, + rules: FaultTransformationRule[] = allRules, + options: FaultTransformationOptions = { + substituteWithComment: false, + }, + ) { + for (const rule of rules) { + const transformation = this.getFaultTransformations( + tree, + [rule], + options, + ); + this.applyCodeTransformationsToTree(tree, transformation); + tree.remountTree(); + } } } diff --git a/src/index.ts b/src/index.ts index 7059589..13a0c5a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,7 @@ export { } from './transforming/fault-transformation-rule.js'; export { RulesDictionary, + RuleId, AccessibilityModifierRemover, ImageDecorativeLabelRemover, } from './transforming/rules-dictionary.js'; diff --git a/src/parsing/swift-file-tree.ts b/src/parsing/swift-file-tree.ts index b029edf..b004c03 100644 --- a/src/parsing/swift-file-tree.ts +++ b/src/parsing/swift-file-tree.ts @@ -180,7 +180,7 @@ export class SwiftFileTree { /** * Replace the trivia between two nodes with the given replacement text. - * Prefer using `replaceNode` with the `clearLeadingTrivia` or `clearTrailingTrivia` options instead of this method. + * Prefer using {@link replaceNode} with the `clearLeadingTrivia` or `clearTrailingTrivia` options instead of this method. */ replaceTriviaBetweenNodes( previousNode: SyntaxNode, diff --git a/src/transforming/code-transformation.ts b/src/transforming/code-transformation.ts index f2b9626..1b9ee35 100644 --- a/src/transforming/code-transformation.ts +++ b/src/transforming/code-transformation.ts @@ -28,4 +28,7 @@ export interface NodeChange { * * @category Transforming */ -export type CodeTransformation = NodeChange[]; +export type CodeTransformation = { + ruleId: string; + nodeChanges: NodeChange[]; +}; diff --git a/src/transforming/rules-dictionary.ts b/src/transforming/rules-dictionary.ts index 6e82905..a121c6b 100644 --- a/src/transforming/rules-dictionary.ts +++ b/src/transforming/rules-dictionary.ts @@ -1,4 +1,3 @@ -import { FaultTransformationRule } from './fault-transformation-rule.js'; import { AccessibilityModifierRemover } from './rules/accessibility-modifier-remover.js'; import { ImageDecorativeLabelRemover } from './rules/image-decorative-label-remover.js'; @@ -7,9 +6,16 @@ import { ImageDecorativeLabelRemover } from './rules/image-decorative-label-remo * * @category Transforming */ -export const RulesDictionary: Record = { +export const RulesDictionary = { AccessibilityModifierRemover, ImageDecorativeLabelRemover, }; +/** + * The ID of a fault transformation rule. + * + * @category Transforming + */ +export type RuleId = keyof typeof RulesDictionary; + export { AccessibilityModifierRemover, ImageDecorativeLabelRemover }; diff --git a/src/transforming/rules/accessibility-modifier-remover.ts b/src/transforming/rules/accessibility-modifier-remover.ts index b514f41..12855ba 100644 --- a/src/transforming/rules/accessibility-modifier-remover.ts +++ b/src/transforming/rules/accessibility-modifier-remover.ts @@ -72,15 +72,18 @@ export const AccessibilityModifierRemover: FaultTransformationRule = { ); }; - return nodes - .map((node) => ({ - node, - replaceWith: getReplaceWith(node), - replaceOptions: { - clearLeadingTrivia: shouldClearLeadingTrivia(node), - clearTrailingTrivia: false, - }, - })) - .reverse(); + return { + ruleId: this.id, + nodeChanges: nodes + .map((node) => ({ + node, + replaceWith: getReplaceWith(node), + replaceOptions: { + clearLeadingTrivia: shouldClearLeadingTrivia(node), + clearTrailingTrivia: false, + }, + })) + .reverse(), + }; }, }; diff --git a/src/transforming/rules/image-decorative-label-remover.ts b/src/transforming/rules/image-decorative-label-remover.ts index 0ca374a..a2b9dfc 100644 --- a/src/transforming/rules/image-decorative-label-remover.ts +++ b/src/transforming/rules/image-decorative-label-remover.ts @@ -1,5 +1,5 @@ import { QueryMatch, SyntaxNode } from 'web-tree-sitter'; -import { CodeTransformation } from '../code-transformation.js'; +import { CodeTransformation, NodeChange } from '../code-transformation.js'; import { FaultTransformationOptions, FaultTransformationRule, @@ -58,7 +58,7 @@ export const ImageDecorativeLabelRemover: FaultTransformationRule = { ): CodeTransformation { const [node] = this.getTransformableNodes(match); - return [ + const nodeChanges: NodeChange[] = [ { node: node.children[0]!, replaceWith: options?.substituteWithComment @@ -78,5 +78,10 @@ export const ImageDecorativeLabelRemover: FaultTransformationRule = { }, }, ].reverse(); + + return { + ruleId: this.id, + nodeChanges, + }; }, }; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..5b38343 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,8 @@ +import { NodeChange } from './transforming/code-transformation.js'; + +/** + * Compare two node changes by their position in the code. + */ +export function byNodePosition(a: NodeChange, b: NodeChange) { + return a.node.endIndex - b.node.endIndex; +} diff --git a/tests/accessibility-modifier-remover.spec.ts b/tests/accessibility-modifier-remover.spec.ts deleted file mode 100644 index 617fc1d..0000000 --- a/tests/accessibility-modifier-remover.spec.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { Deaccessibilizer } from '../src/deaccessibilizer.js'; -import { - readFileContent, - SWIFT_FILE_SAMPLES_BASE_PATH, -} from './utils/get-text-from-file.js'; -import { AccessibilityModifierRemover } from '../src/index.js'; - -const deaccessibilizer = new Deaccessibilizer(); -const rule = AccessibilityModifierRemover; - -describe('AccessibilityModifierRemover', () => { - it('runs the rule successfully for Hildete example', async () => { - const code = readFileContent( - `${SWIFT_FILE_SAMPLES_BASE_PATH}/Hildete.swift`, - ); - const tree = await deaccessibilizer.createSwiftFileTree(code); - - const query = tree.queryNode(tree.swiftUIViews[0], rule.queryText); - query.reverse().forEach((match) => { - const transformation = rule.getFaultTransformation(match, { - substituteWithComment: false, - }); - transformation.forEach((change) => { - tree.replaceNode( - change.node, - change.replaceWith, - change.replaceOptions, - ); - }); - }); - - const expectedCode = readFileContent( - `${SWIFT_FILE_SAMPLES_BASE_PATH}/Hildete_noAccessibilityModifier.swift`, - ); - expect(tree.text).toBe(expectedCode); - }); -}); diff --git a/tests/deaccessibilizer.spec.ts b/tests/deaccessibilizer.spec.ts new file mode 100644 index 0000000..8e6d040 --- /dev/null +++ b/tests/deaccessibilizer.spec.ts @@ -0,0 +1,11 @@ +import { describe, expect, it } from 'vitest'; +import { Deaccessibilizer } from '../src/deaccessibilizer.js'; + +const deaccessibilizer = new Deaccessibilizer(); + +describe('Deaccessibilizer', () => { + it('successfully builds a Swift file tree', async () => { + const tree = await deaccessibilizer.createSwiftFileTree('let x = 3'); + expect(tree).toBeDefined(); + }); +}); diff --git a/tests/rules/accessibility-modifier-remover.spec.ts b/tests/rules/accessibility-modifier-remover.spec.ts new file mode 100644 index 0000000..8487ba4 --- /dev/null +++ b/tests/rules/accessibility-modifier-remover.spec.ts @@ -0,0 +1,23 @@ +import { describe, it } from 'vitest'; +import { + Deaccessibilizer, + AccessibilityModifierRemover, +} from '../../src/index.js'; +import { SWIFT_FILE_SAMPLES_BASE_PATH } from '../utils/get-text-from-file.js'; +import { getExpectIsSameFileAfterApplyingRules } from '../utils/expect-is-same-file-after-applying-rules.js'; + +const deaccessibilizer = new Deaccessibilizer(); +const expectIsSameFileAfterApplyingRules = + getExpectIsSameFileAfterApplyingRules(deaccessibilizer); + +const rule = AccessibilityModifierRemover; + +describe('AccessibilityModifierRemover', () => { + it('runs the rule successfully for Hildete example', async () => { + await expectIsSameFileAfterApplyingRules( + `${SWIFT_FILE_SAMPLES_BASE_PATH}/Hildete.swift`, + `${SWIFT_FILE_SAMPLES_BASE_PATH}/Hildete_noAccessibilityModifier.swift`, + [rule], + ); + }); +}); diff --git a/tests/rules/image-decorative-label-remover.spec.ts b/tests/rules/image-decorative-label-remover.spec.ts new file mode 100644 index 0000000..afb8c7b --- /dev/null +++ b/tests/rules/image-decorative-label-remover.spec.ts @@ -0,0 +1,31 @@ +import { describe, it } from 'vitest'; +import { + Deaccessibilizer, + ImageDecorativeLabelRemover, +} from '../../src/index.js'; +import { SWIFT_FILE_SAMPLES_BASE_PATH } from '../utils/get-text-from-file.js'; +import { getExpectIsSameFileAfterApplyingRules } from '../utils/expect-is-same-file-after-applying-rules.js'; + +const deaccessibilizer = new Deaccessibilizer(); +const expectIsSameFileAfterApplyingRules = + getExpectIsSameFileAfterApplyingRules(deaccessibilizer); + +const rule = ImageDecorativeLabelRemover; + +describe('ImageDecorativeLabelRemover', () => { + it('runs the rule successfully for Enzo example', async () => { + await expectIsSameFileAfterApplyingRules( + `${SWIFT_FILE_SAMPLES_BASE_PATH}/Enzo.swift`, + `${SWIFT_FILE_SAMPLES_BASE_PATH}/Enzo_noImageDecorativeLabel.swift`, + [rule], + ); + }); + + it('is no-op for Hildete example', async () => { + await expectIsSameFileAfterApplyingRules( + `${SWIFT_FILE_SAMPLES_BASE_PATH}/Hildete.swift`, + `${SWIFT_FILE_SAMPLES_BASE_PATH}/Hildete.swift`, + [rule], + ); + }); +}); diff --git a/tests/swift-file-tree.spec.ts b/tests/swift-file-tree.spec.ts index 72a7e0d..0e15ccb 100644 --- a/tests/swift-file-tree.spec.ts +++ b/tests/swift-file-tree.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { Deaccessibilizer } from '../src/deaccessibilizer.js'; +import { Deaccessibilizer } from '../src/index.js'; import { readFileContent, SWIFT_FILE_SAMPLES_BASE_PATH, @@ -8,11 +8,6 @@ import { const deaccessibilizer = new Deaccessibilizer(); describe('SwiftFileTree', () => { - it('successfully builds a Swift file tree', async () => { - const tree = await deaccessibilizer.createSwiftFileTree('let x = 3'); - expect(tree).toBeDefined(); - }); - it('finds SwiftUI components when they exist', async () => { const code = readFileContent( `${SWIFT_FILE_SAMPLES_BASE_PATH}/Hildete.swift`, @@ -28,4 +23,21 @@ describe('SwiftFileTree', () => { const tree = await deaccessibilizer.createSwiftFileTree(code); expect(tree.swiftUIViews).toHaveLength(0); }); + + it('replaces text', async () => { + const tree = await deaccessibilizer.createSwiftFileTree('let x = 3'); + + tree.replaceText({ row: 0, column: 4 }, { row: 0, column: 5 }, 4, 5, 'y'); + + expect(tree.text).toBe('let y = 3'); + }); + + it('replaces node with text', async () => { + const tree = await deaccessibilizer.createSwiftFileTree('let x = 3'); + + const integerLiteral = tree.tree.rootNode.children[0].children[3]; + tree.replaceNode(integerLiteral, '500'); + + expect(tree.text).toBe('let x = 500'); + }); }); diff --git a/tests/utils/expect-is-same-file-after-applying-rules.ts b/tests/utils/expect-is-same-file-after-applying-rules.ts new file mode 100644 index 0000000..631efa9 --- /dev/null +++ b/tests/utils/expect-is-same-file-after-applying-rules.ts @@ -0,0 +1,34 @@ +import { expect } from 'vitest'; +import { + Deaccessibilizer, + FaultTransformationOptions, + FaultTransformationRule, +} from '../../src/index.js'; +import { readFileContent } from './get-text-from-file.js'; + +/** + * Returns a function that expects the file content of a file to be the same of + * another one after applying the given fault transformation rules. + */ +export function getExpectIsSameFileAfterApplyingRules( + deaccessibilizer: Deaccessibilizer, +) { + return async ( + baseFilePath: string, + expectedFilePath: string, + rules: FaultTransformationRule[], + options?: FaultTransformationOptions, + ) => { + const baseCode = readFileContent(baseFilePath); + const expectedCode = readFileContent(expectedFilePath); + const tree = await deaccessibilizer.createSwiftFileTree(baseCode); + + deaccessibilizer.applyFaultTransformationsToTreeWithRebuild( + tree, + rules, + options, + ); + + expect(tree.text).toBe(expectedCode); + }; +} diff --git a/tests/utils/get-text-from-file.ts b/tests/utils/get-text-from-file.ts index d81ec64..4314b9e 100644 --- a/tests/utils/get-text-from-file.ts +++ b/tests/utils/get-text-from-file.ts @@ -1,7 +1,13 @@ import fs from 'fs'; +/** + * Read the content of a file. + */ export function readFileContent(filePath: string): string { return fs.readFileSync(filePath, 'utf8'); } +/** + * The base path for the Swift file samples. + */ export const SWIFT_FILE_SAMPLES_BASE_PATH = './misc/swift-file-samples/';