From 3cedd4664f9e89546bb605df562e4061f4a84657 Mon Sep 17 00:00:00 2001 From: Brian Hall Date: Tue, 14 Jan 2025 14:43:01 +0100 Subject: [PATCH] Add support for conditional clicking (#3623) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Task/Issue URL: https://app.asana.com/0/1199230911884351/1208647902506360/f Tech Design URL: CC: **Description**: Adds support for conditional clicking of elements by adding “choices” and “default” keys to the click action. **Optional E2E tests**: - [x] Run PIR E2E tests Check this to run the Personal Information Removal end to end tests. If updating CCF, or any PIR related code, tick this. **Steps to test this PR**: The easiest way to test whether the decoding is working is to setup one of the test files from the C-S-S integration tests as a fake broker: 1. Download [this file](https://github.com/duckduckgo/content-scope-scripts/blob/main/injected/integration-test/test-pages/broker-protection/pages/expectation-actions.html) on your local machine. Open it to copy the file path, should start with `file://` 2. Download [this JSON](https://gist.github.com/brianhall/969d81b04b8d4fff4e21c9c6b48164b9) and update the url to the file path you copied in 1. 3. Save the JSON file in the macos-browser repo under `LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Resources/JSON` 4. Build the browser (no need for any special dependencies, just build `main`), then open the debug version of PIR: Debug Menu -> Personal Information Removal -> Run Personal Information Removal in Debug Mode. 5. In Birth Year enter `1965`, then select **Test Broker** in the list of brokers and click the Run button. 6. After about 30 seconds, right click the webview and Inspect Element, then click the console tab and type `window.location.href` and hit enter. You should see a url that ends with `#1`, which means that the user was older than 45. 7. Do steps 5-6 again, but for birth year enter `1990`, when you do `window.location.href` you should get a url that ends with `#2` instead, indicating that the user is < 45. 8. Edit the mybrokertest.com.json file, copy/paste the contents of [this gist](https://gist.github.com/brianhall/ebac912c56c5e904ec86c05e3fa3ab30) in and save. This just sets the default case at the bottom to `null`. _Now re-build the browser to load the updated JSON._ 9. Run steps 5-7 again, this time `window.location.href` should return a url with `#1` for step 5, and a url with no `#` at the end for step 7, meaning that nothing was clicked and no error was triggered. 10. Edit the mybrokertest.com.json file again and remove line 45 (and the comma above on line 44) and then rebuild the browser to update the JSON. Finally steps 5-7 again - step 5 should return no errors (and have a `#1` hash at the end of the url), and step 7 should not return an error, but also not have any hash (e.g. `#1`) at the end of the url. **Definition of Done**: * [ ] Does this PR satisfy our [Definition of Done](https://app.asana.com/0/1202500774821704/1207634633537039/f)? — ###### Internal references: [Pull Request Review Checklist](https://app.asana.com/0/1202500774821704/1203764234894239/f) [Software Engineering Expectations](https://app.asana.com/0/59792373528535/199064865822552) [Technical Design Template](https://app.asana.com/0/59792373528535/184709971311943) [Pull Request Documentation](https://app.asana.com/0/1202500774821704/1204012835277482/f) --- .../Model/Actions/Click.swift | 66 ++++++++++++++++++- .../DataBrokerOperationActionTests.swift | 2 +- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/Actions/Click.swift b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/Actions/Click.swift index a75fe0113f..e237d32bfb 100644 --- a/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/Actions/Click.swift +++ b/LocalPackages/DataBrokerProtection/Sources/DataBrokerProtection/Model/Actions/Click.swift @@ -18,9 +18,73 @@ import Foundation +struct Condition: Codable, Sendable { + let left: String + let operation: String + let right: String +} + +struct Choice: Codable, Sendable { + let condition: Condition + let elements: [PageElement] +} + struct ClickAction: Action { let id: String let actionType: ActionType - let elements: [PageElement] + let elements: [PageElement]? let dataSource: DataSource? + let choices: [Choice]? + let `default`: Default? + + let hasDefault: Bool + + struct Default: Codable { + let elements: [PageElement]? + } + + init(id: String, actionType: ActionType, elements: [PageElement]? = nil, dataSource: DataSource? = nil, choices: [Choice]? = nil, `default`: Default? = nil, hasDefault: Bool = false) { + self.id = id + self.actionType = actionType + self.elements = elements + self.dataSource = dataSource + self.choices = choices + self.default = `default` + self.hasDefault = `default` != nil + } + + init(from decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.id = try container.decode(String.self, forKey: .id) + self.actionType = try container.decode(ActionType.self, forKey: .actionType) + self.elements = try container.decodeIfPresent([PageElement].self, forKey: .elements) + self.dataSource = try container.decodeIfPresent(DataSource.self, forKey: .dataSource) + self.choices = try container.decodeIfPresent([Choice].self, forKey: .choices) + self.default = try container.decodeIfPresent(Default.self, forKey: .default) + self.hasDefault = container.contains(.default) + } + + enum CodingKeys: String, CodingKey { + case id + case actionType + case elements + case dataSource + case choices + case `default` + } + + func encode(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(self.id, forKey: .id) + try container.encode(self.actionType, forKey: .actionType) + try container.encodeIfPresent(self.elements, forKey: .elements) + try container.encodeIfPresent(self.dataSource, forKey: .dataSource) + try container.encodeIfPresent(self.choices, forKey: .choices) + + if self.hasDefault { + try container.encode(self.default, forKey: .default) + } else { + try container.encodeNil(forKey: .default) + } + } } diff --git a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationActionTests.swift b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationActionTests.swift index cc14f56b56..e5c816432c 100644 --- a/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationActionTests.swift +++ b/LocalPackages/DataBrokerProtection/Tests/DataBrokerProtectionTests/DataBrokerOperationActionTests.swift @@ -394,7 +394,7 @@ final class DataBrokerOperationActionTests: XCTestCase { func testWhenClickActionRuns_thenStageIsSetToSubmit() async { let mockStageCalculator = MockStageDurationCalculator() - let clickAction = ClickAction(id: "1", actionType: .click, elements: [PageElement](), dataSource: nil) + let clickAction = ClickAction(id: "1", actionType: .click, elements: [PageElement](), dataSource: nil, choices: nil, default: nil, hasDefault: false) let sut = OptOutJob( privacyConfig: PrivacyConfigurationManagingMock(), prefs: ContentScopeProperties.mock,