{"id":18562022,"url":"https://github.com/mangoweb/spectools","last_synced_at":"2025-04-10T03:31:24.304Z","repository":{"id":56230652,"uuid":"101411769","full_name":"manGoweb/SpecTools","owner":"manGoweb","description":"Write less test code with this set of spec tools. Swift, iOS, testing framework independent (but works well with Quick/Nimble or directly).","archived":false,"fork":false,"pushed_at":"2023-07-19T20:21:28.000Z","size":1512,"stargazers_count":39,"open_issues_count":2,"forks_count":6,"subscribers_count":8,"default_branch":"master","last_synced_at":"2025-04-08T05:19:39.707Z","etag":null,"topics":["appletv","ios","ipad","iphone","swift","testing","testing-framework","testing-tools","ui-testing","watchos"],"latest_commit_sha":null,"homepage":"","language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/manGoweb.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-08-25T14:27:45.000Z","updated_at":"2024-03-13T11:23:39.000Z","dependencies_parsed_at":"2024-11-06T22:18:42.140Z","dependency_job_id":null,"html_url":"https://github.com/manGoweb/SpecTools","commit_stats":{"total_commits":74,"total_committers":13,"mean_commits":"5.6923076923076925","dds":0.5945945945945945,"last_synced_commit":"b7a49d3c0faf961138c6fbcc9b972ec4a5c10266"},"previous_names":[],"tags_count":15,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/manGoweb%2FSpecTools","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/manGoweb%2FSpecTools/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/manGoweb%2FSpecTools/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/manGoweb%2FSpecTools/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/manGoweb","download_url":"https://codeload.github.com/manGoweb/SpecTools/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248150939,"owners_count":21055999,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["appletv","ios","ipad","iphone","swift","testing","testing-framework","testing-tools","ui-testing","watchos"],"created_at":"2024-11-06T22:08:35.626Z","updated_at":"2025-04-10T03:31:19.175Z","avatar_url":"https://github.com/manGoweb.png","language":"Swift","funding_links":[],"categories":[],"sub_categories":[],"readme":"![Alamofire: Elegant Networking in Swift](https://raw.githubusercontent.com/manGoweb/SpecTools/master/Assets/Icon.png)\n\n# SpecTools\n\n[![Slack](https://img.shields.io/badge/join-slack-745EAF.svg?style=flat)](http://bit.ly/2B0dEyt)\n[![CircleCI](https://img.shields.io/circleci/project/github/manGoweb/SpecTools/master.svg?style=flat)](https://circleci.com/gh/manGoweb/SpecTools)\n[![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage)\n[![Version](https://img.shields.io/cocoapods/v/SpecTools.svg?style=flat)](http://cocoapods.org/pods/SpecTools)\n[![License](https://img.shields.io/cocoapods/l/SpecTools.svg?style=flat)](http://cocoapods.org/pods/SpecTools)\n[![Platform](https://img.shields.io/cocoapods/p/SpecTools.svg?style=flat)](http://cocoapods.org/pods/SpecTools)\n[![Docs](http://mangoweb.github.io/SpecTools/docs/latest/badge.svg)](http://mangoweb.github.io/SpecTools/docs/latest/)\n[![Twitter](https://img.shields.io/badge/twitter-@rafiki270-blue.svg?style=flat)](http://twitter.com/rafiki270)\n\n## \n\nLibrary that helps you write less code when testing interface in your iOS apps.\n\n## Slack\n\nGet help using and installing this product on our [Slack](http://bit.ly/2B0dEyt), channel \u003cb\u003e#help-spectools\u003c/b\u003e\n\n## Implementation\n\nAfter you add SpecTools framework a set of options will become available for most of the UI elements through a spec property. Available for iOS and tvOS \n\nThese are:\n* action\n    * simulate tap on a button with specific action event\n    * simulate gesture recognizer taps\n* check\n    * if a view is truly visible on screen\n    * view controllers and navigation stacks\n    * table views have the right content in their cells\n* find\n    * locate any element or elements on the screen based on their content or type\n    * any text on any UI element\n* prepare\n    * prepare your view controller like it would get setup in a real environment\n    * change size of the view for any device screen during runtime to check all is still visible\n    * assign your test view controllers to a mock navigation controller in order to track `pushViewController` or `popViewController` methods\n\nThere is a space for a lot more additional functionality so please feel free to raise issues with feature requests.\n\nAn example implementation is shown below:\n\n```Swift\nimport Foundation\nimport UIKit\nimport Quick\nimport Nimble\nimport SpecTools\n\n@testable import SpecToolsExample\n\n\nclass ViewControllerSpec: QuickSpec {\n    \n    override func spec() {\n        \n        let subject = ViewController()\n        \n        describe(\"basic view controller\") {\n            beforeEach {\n                // Simulate view controller being presented to the screen\n                subject.spec.prepare.simulatePresentViewController()\n                // Reset the view to specific size\n                subject.spec.prepare.set(viewSize: .iPhone6Plus)\n            }\n            \n            it(\"has a visible label1\") {\n                // Get your first label\n                let element = subject.view.spec.find.first(labelWithText: \"My first label\")\n                // Check if the label is truly visible and print out the entire view structure that is being checked\n                expect(element?.spec.check.isVisible(visualize: .text)).to(beTrue())\n            }\n            \n            it(\"has a visible scrollView\") {\n                // Get a scroll view\n                let element = subject.view.spec.find.firstScrollView()\n                // Check visibility\n                expect(element?.spec.check.isVisible()).to(beTrue())\n            }\n            \n            it(\"has a visible label2\", closure: {\n                // Get a label that contains \"second label\" and print how we get to it in the console including any text on visible elements\n                let element = subject.view.spec.find.first(labelWithText: \"My second label\", exactMatch: false, visualize: .text)\n                // Check if the label is visible on subjects view and print all frames we encounter on the way\n                expect(element?.spec.check.isVisible(on: subject.view, visualize: .frames)).to(beTrue())\n            })\n            \n            describe(\"when we tap on button1\") {\n                beforeEach {\n                    // Simulate button tap\n                    button1.spec.action.tap()\n                }\n            \n                it(\"should have pushed new view controller\") {\n                    // Check we have new view controller in the navigation stack\n                    expect(subject.navigationController?.spec.check.contains(viewControllerClass: TableViewController.self)).to(beTrue())\n                }\n            }\n            \n        }\n        \n    }\n    \n}\n```\n\n## Demo app with tests\n\nTo run the example project: \n* Clone the repo, and run `pod install` from the Example directory.\n* Cmd+U to run example tests\n\n## Documentation\n\nJazzy based documentation is available here [![Docs](http://mangoweb.github.io/SpecTools/docs/latest/badge.svg)](http://mangoweb.github.io/SpecTools/docs/latest/).\n\nOnline documentation should always reflect the latest code available on `master` branch.\n\n## Requirements\n\nThis library can be run completely independently. It does not need quick and nimble although we highly recommend your give these libraries a go!\n\n#### Cocoapods\n\nSpecTools is available through [CocoaPods](http://cocoapods.org). To install\nit, simply add the following line to your test target in a Podfile:\n\n```ruby\ntarget 'SpecToolsExampleTests' do\n\tpod \"SpecTools\"\nend\n```\n\n#### Carthage\n\nSpecTools is also available through [Carthage](https://github.com/Carthage/Carthage). To install\nit, simply add the following line to your Cartfile and than import the framework into your test target.\n```ruby\ngithub \"manGoweb/SpecTools\"\n```\n\n## Usage\n\n### Debugging\n\nSome methods have a debugging mechanisms available. The most common one is `visualize: VisualizationType` parameter which allows you to print out debug data (usually a tree of elements SpecTools have recursed through) into the console.\n\nFor the visualize options available please refer to the documentation: [VisualizationType](http://mangoweb.github.io/SpecTools/docs/latest/Enums/VisualizationType.html)\n\nFollowing section should contain all of the methods available\n\n### Actions\n\n#### Simulating taps\n\nSimulate tap on a UIButton\n```Swift\nbutton1.spec.action.tap()\n// or\nbutton1.spec.action.tap(event: .touchUpInside)\n```\n------\nSimulate tap on a view with UITapGestureRecognizer(s)\n```Swift\nview.spec.action.triggerTap()\n// or\nview.spec.action.triggerTap(taps: 3, touches: 2)\n```\n------\nSimulate tap on a UITableView cell\n```Swift\ntableView.spec.action.tap(row: 6)\n// or\ntableView.spec.action.tap(row: 1, section: 5)\n```\n------\nSimulate tap on a UICollectionView cell\n```Swift\ncollectionView.spec.action.tap(item: 3)\n// or\ncollectionView.spec.action.tap(item: 2, section: 1)\n```\n------\n#### Simulating swipes\n\nSimulate swipe on a view with UISwipeGestureRecognizer(s)\n```Swift\nview.spec.action.triggerSwipe(direction: .up)\n```\n\n------\n#### Simulating long presses\n\nSimulate swipe on a view with UILongPressGestureRecognizer(s)\n```Swift\nbutton.spec.action.triggerLongPress()\n// or\nbutton.spec.action.triggerLongPress(minimumPressDuration: 1.0, taps: 3, touches: 2)\n```\n------\n\n#### Executing gesture recognizers (not available on tvOS)\n\nExecute action on any UIGestureRecognizer\n```Swift\nrecognizer.spec.action.execute()\n```\n------\nGet array of targets from any UIGestureRecognizer\n - Return `[(target: AnyObject, action: Selector)]`\n```Swift\nrecognizer.spec.action.getTargetInfo()\n```\n\n#### Simulating scrolls\n\nSimulate scrolling on any UIScrollView (or table/collection view) while calling all available delegate methods in the right order along the way\n - decelerate sets if the scroll view should simulate decelerating after dragging\n```Swift\nscrollView.spec.action.scroll(to: CGPoint(x: 500, y: 0), decelerate: true)\n```\n------\nSimulate scrolling on any UIScrollView to a specific horizontal page\n```Swift\nscrollView.spec.action.scroll(horizontalPageIndex: 2, decelerate: false)\n```\n------\nSimulate scrolling on any UIScrollView to a specific vertical page\n```Swift\nscrollView.spec.action.scroll(verticalPageIndex: 5, decelerate: true)\n```\n------\n\n### Checks\n\n#### Checking view visibility\n\nIs view truly visible on the screen? Checks if the element (and all parent views) have superview, have alpha, are not hidden and have a valid on-screen frame.\n```Swift\nview.spec.check.isVisible() // Example 1\n// or\nview.spec.check.isVisible(on: viewController.view, visualize: .all) // Example 2\n```\nExample 1) Ignores the last view having no superview, expects it to be view controllers view for example\n\nExample 2) Checks visibility against another view, also an entire recursed view structure can be printed to the console\n\n#### Checking table view cells\n\nCheck if all UITableViewCells available through the UITableView data source evaluate correctly\n```Swift\n// Create an enumerate closure\nlet doesFitClosure: (UITableViewCell)-\u003eBool = { (cell) -\u003e Bool in\n\tguard let cell = cell as? CustomTableViewCell else {\n\t\treturn false\n\t}\n\tif cell.customLabel.text?.characters.count == 0 {\n\t\treturn false\n\t}\n\treturn true\n}\n\n// Test if all cells generated by the data source are ok using your closure\nlet ok = subject.tableView.spec.check.allCells(fit: doesFitClosure)\n```\n------\nYou can also ask for an array of IndexPaths that don't fit the criteria\n```Swift\nlet indexPaths: [IndexPath] = subject.tableView.spec.check.allCells(thatDontFit: doesFitClosure)\n```\n\n#### Checking collection view cells\n\nCheck if all UICollectionViewCell available through the UICollectionView data source evaluate correctly\n```Swift\n// Create an enumerate closure\nlet doesFitClosure: (UICollectionViewCell)-\u003eBool = { (cell) -\u003e Bool in\n\tguard let cell = cell as? CustomCollectionViewCell else {\n\t\treturn false\n\t}\n\tif cell.customLabel.text?.characters.count == 0 {\n\t\treturn false\n\t}\n\treturn true\n}\n\n// Test if all cells generated by the data source are ok using your closure\nlet ok = subject.collectionView.spec.check.allCells(fit: doesFitClosure)\n```\n------\nYou can also ask for an array of IndexPaths that don't fit the criteria\n```Swift\nlet indexPaths: [IndexPath] = subject.collectionView.spec.check.allCells(thatDontFit: doesFitClosure)\n```\n\n#### UIViewController checks\n\nLook for a specific view controller in your navigation stack\n```Swift\nlet ok: Bool = viewController.spec.check.has(siblingInNavigationStack: anotherViewController)\n```\n------\nCheck if a view controller has a child view controller\n```Swift\nlet ok: Bool = viewController.spec.check.has(childViewController: childViewController)\n```\n------\nCheck if a view controller has a specific class type in its navigation stack\n```Swift\nlet ok: Bool = viewController.spec.check.contains(siblingClassInNavigationStack: AnotherViewController.self)\n```\n------\nCheck if a view controller has specific class type of a child view controller\n```Swift\nlet ok: Bool = viewController.spec.check.contains(childViewControllerClass: ChildViewController.self)\n```\n\n#### UINavigationController checks\n\nCheck is navigation view controller contains certain view controller\n```Swift\nlet ok: Bool = navigationController.spec.check.has(viewController: vc)\n```\n------\nCheck is navigation view controller contains certain type of a view controller\n```Swift\nlet ok: Bool = viewController.spec.check.contains(viewControllerClass: MyCustomViewController.self)\n```\n\n### Find\n\n#### Gesture recognizers\n\nFind all gesture recognizers of a certain type on a view (generic method)\n```Swift\nlet recognizers: [UISwipeGestureRecognizer] = view.spec.find.all(gestureRecognizersOfType: UISwipeGestureRecognizer.self)\n```\n\n#### UIKit elements\n\nFind first label (UILabel) on a specific view, which contains or matches required string. To search for an element by it's only a partial string/content, use `exactMatch: Bool` flag. To visualize the path to the element, use `visualize: VisualizationType`. Please refer to the [Debugging](#debugging) section for more details.\n```Swift\nlet element = view.spec.find.first(labelWithText: \"My first label\")\n// or\nlet element = view.spec.find.first(labelWithText: \"first\", exactMatch: false, visualize: .text)\n```\n------\nFind first text field (UITextField) on a specific view, which contains or matches required string. To search for an element by it's only a partial string/content, use `exactMatch: Bool` flag. To visualize the path to the element, use `visualize: VisualizationType`. Please refer to the [Debugging](#debugging) section for more details.\n```Swift\nlet element = view.spec.find.first(textFieldWithText: \"My first text field\")\n// or\nlet element = view.spec.find.first(textFieldWithText: \"first\", exactMatch: false, visualize: .text)\n```\n------\nFind first search bar (UISearchBar) on a specific view, which contains or matches required string. To search for an element by it's only a partial string/content, use `exactMatch: Bool` flag. To visualize the path to the element, use `visualize: VisualizationType`. Please refer to the [Debugging](#debugging) section for more details.\n```Swift\nlet element = view.spec.find.first(searchBarWithText: \"My first search bar\")\n// or\nlet element = view.spec.find.first(searchBarWithText: \"first\", exactMatch: false, visualize: .text)\n```\n------\nFind first text view (UITextView) on a specific view, which contains or matches required string. To search for an element by it's only a partial string/content, use `exactMatch: Bool` flag. To visualize the path to the element, use `visualize: VisualizationType`. Please refer to the [Debugging](#debugging) section for more details.\n```Swift\nlet element = view.spec.find.first(textViewWithText: \"My first text view\")\n// or\nlet element = view.spec.find.first(textViewWithText: \"first\", exactMatch: false, visualize: .text)\n```\n------\nFind first table view cell (UITableViewCell) on a specific view, which contains or matches required string. To search for an element by it's only a partial string/content, use `exactMatch: Bool` flag. To visualize the path to the element, use `visualize: VisualizationType`. Please refer to the [Debugging](#debugging) section for more details.\n```Swift\nlet element = view.spec.find.first(tableViewCellWithText: \"My first cell\")\n// or\nlet element = view.spec.find.first(tableViewCellWithText: \"first\", exactMatch: false, visualize: .text)\n```\n------\nFind first button (UIButton) on a specific view, which contains or matches required string. To search for an element by it's only a partial string/content, use `exactMatch: Bool` flag. To visualize the path to the element, use `visualize: VisualizationType`. Please refer to the [Debugging](#debugging) section for more details.\n```Swift\nlet element = view.spec.find.first(buttonWithText: \"My first button\")\n// or\nlet element = view.spec.find.first(buttonWithText: \"first\", exactMatch: false, visualize: .text)\n```\n------\nFind first table header or footer view (UITableViewHeaderFooterView) on a specific view, which contains or matches required string. To search for an element by it's only a partial string/content, use `exactMatch: Bool` flag. To visualize the path to the element, use `visualize: VisualizationType`. Please refer to the [Debugging](#debugging) section for more details.\n```Swift\nlet element = view.spec.find.first(tableSectionHeaderFooterViewWithText: \"My first table header or footer view\")\n// or\nlet element = view.spec.find.first(tableSectionHeaderFooterViewWithText: \"first\", exactMatch: false, visualize: .text)\n```\n------\nFind first UITableView's header or footer (not a section header or footer) on a specific view, which contains or matches required string. View you are searching on can hold multiple table views. To search for an element by it's only a partial string/content, use `exactMatch: Bool` flag. To visualize the path to the element, use `visualize: VisualizationType`. Please refer to the [Debugging](#debugging) section for more details.\n```Swift\nlet element = view.spec.find.first(tableHeaderFooterViewWithText: \"My first header view\")\n// or\nlet element = view.spec.find.first(tableHeaderFooterViewWithText: \"first\", exactMatch: false, visualize: .text)\n```\n------\nFind first view (any UIView) on a specific parent view, which contains or matches required string. To search for an element by it's only a partial string/content, use `exactMatch: Bool` flag. To visualize the path to the element, use `visualize: VisualizationType`. Please refer to the [Debugging](#debugging) section for more details.\n```Swift\nlet element = view.spec.find.first(elementWithText: \"My first view with some text on a label deep inside\")\n// or\nlet element = view.spec.find.first(elementWithText: \"deep\", exactMatch: false, visualize: .text)\n```\n------\nFind first element (any UIView subclass for Element) on a specific view, which contains or matches required string. To search for an element by it's only a partial string/content, use `exactMatch: Bool` flag. To visualize the path to the element, use `visualize: VisualizationType`. Please refer to the [Debugging](#debugging) section for more details.\n```Swift\nlet element = view.spec.find.first(elementOfType: MyCustomView.self, withText: \"My first custom view with some text on a label deep inside\")\n// or\nlet element = view.spec.find.first(elementOfType: UILabel.self, withText: \"custom\", exactMatch: false, visualize: .text)\n```\n------\nFind first table view (UITableView) on a specific view. To visualize the path to the element, use `visualize: VisualizationType`. Please refer to the [Debugging](#debugging) section for more details.\n```Swift\nlet element = view.spec.find.firstTableView()\n// or\nlet element = view.spec.find.firstTableView(visualize: .frames)\n```\n------\nFind first collection view (UICollectionView) on a specific view. To visualize the path to the element, use `visualize: VisualizationType`. Please refer to the [Debugging](#debugging) section for more details.\n```Swift\nlet element = view.spec.find.firstCollectionView()\n// or\nlet element = view.spec.find.firstCollectionView(visualize: .frames)\n```\n------\nFind first scroll view (UIScrollView) on a specific view. To visualize the path to the element, use `visualize: VisualizationType`. Please refer to the [Debugging](#debugging) section for more details.\n```Swift\nlet element = view.spec.find.firstScrollView()\n// or\nlet element = view.spec.find.firstScrollView(visualize: .frames)\n```\n------\nFind first table header or footer view (UITableViewHeaderFooterView) on a specific view. To visualize the path to the element, use `visualize: VisualizationType`. Please refer to the [Debugging](#debugging) section for more details.\n```Swift\nlet element = view.spec.find.firstTableHeaderFooterView()\n// or\nlet element = view.spec.find.firstTableHeaderFooterView(visualize: .frames)\n```\n------\nFind first element (generic method) on a specific view. To visualize the path to the element, use `visualize: VisualizationType`. Please refer to the [Debugging](#debugging) section for more details.\n```Swift\nlet element = view.spec.find.first(elementOfType: MyCustomView.self)\n// or\nlet element = view.spec.find.first(elementOfType: UIButton.self, visualize: .frames)\n```\n------\nFind all elements of a certain type on a specific view. To visualize the path to the element, use `visualize: VisualizationType`. Please refer to the [Debugging](#debugging) section for more details.\n```Swift\nlet element = view.spec.find.all(elementsOfType: UITextField.self)\n// or\nlet element = view.spec.find.all(elementsOfType: MyCustomView.self, visualize: .frames)\n```\n\n#### Searching for text on some UIKit elements\n\nGet a text from specific view in order of it's importance (first text on a text field, than it looks for placeholder if text is empty). You can specify what text you are looking for by using `preferablyMatching: String`. If no text matching the string is found, method will revert to it's original order of priorities. Method returns nil if no direct text property is available.\n\nThis method only works for `UILabel`, `UITextField`, `UISearchBar` or `UITextView`\n```Swift\nlet element = view.spec.find.anyText()\n// or\nlet element = view.spec.find.anyText(preferablyMatching: \"Welcome to my fun app!\")\n```\n\n### Prepare\n\n#### Prepare view controllers for testing\n\nWill touch view of a view controller in order to get loadView and viewDidLoad called, than manually calls viewWillAppear and viewDidAppear with animations disabled\n```Swift\nviewController.spec.prepare.simulatePresentViewController()\n```\n------\nSet a new, specific size for a view controllers view during runtime\n```Swift\nviewController.spec.prepare.set(viewSize: CGSize(width: 375.0, height: 1500))\n```\n------\nSet a screensize of a desired device on a view of your view controller, you can specify a custom height. Custom height might be useful when scrollviews are present\n```Swift\nviewController.spec.prepare.set(viewSize: .iPhone6Plus)\n// or\nlet customHeight: CGFloat = 5000.0\nviewController.spec.prepare.set(viewSize: .iPhone6Plus, height: customHeight)\n```\n------\nGive view controller a navigation controller\n```Swift\nviewController.spec.prepare.assignNavigationController()\n// or\nviewController.spec.prepare.assignNavigationController(ofClass: CustomNavigationViewController.self)\n```\n------\nGive view controller a mock navigation controller which mainly allows for testing push/pop functionality\n```Swift\nviewController.spec.prepare.assignMockNavigationController()\n```\n\n\n## Author\n\n- Ondrej Rafaj, developers@mangoweb.cz\n\n## Contributors\n\n- Jonathan Augele, jona2k5@yahoo.com\n- David Harris, davidaharris@outlook.com\n- Karol Kozub, karol.kozub@gmail.com\n- Mateusz Szklarek, mateusz.szklarek@icloud.com\n\n## License\n\nSpecTools is available under the MIT license. See the LICENSE file for more info.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmangoweb%2Fspectools","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmangoweb%2Fspectools","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmangoweb%2Fspectools/lists"}