forked from MapComplete/MapComplete
		
	Fix #343, add the poss^Cility to use the test backend (WIP), improve testability of OsmConnection (WIP)
This commit is contained in:
		
							parent
							
								
									8d404b1ba9
								
							
						
					
					
						commit
						9458128ccf
					
				
					 7 changed files with 138 additions and 42 deletions
				
			
		| 
						 | 
					@ -7,6 +7,7 @@ import {ElementStorage} from "../ElementStorage";
 | 
				
			||||||
import Svg from "../../Svg";
 | 
					import Svg from "../../Svg";
 | 
				
			||||||
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
 | 
					import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
 | 
				
			||||||
import Img from "../../UI/Base/Img";
 | 
					import Img from "../../UI/Base/Img";
 | 
				
			||||||
 | 
					import {Utils} from "../../Utils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default class UserDetails {
 | 
					export default class UserDetails {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -22,23 +23,44 @@ export default class UserDetails {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export class OsmConnection {
 | 
					export class OsmConnection {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public static readonly _oauth_configs = {
 | 
				
			||||||
 | 
					        "osm": {
 | 
				
			||||||
 | 
					            oauth_consumer_key: 'hivV7ec2o49Two8g9h8Is1VIiVOgxQ1iYexCbvem',
 | 
				
			||||||
 | 
					            oauth_secret: 'wDBRTCem0vxD7txrg1y6p5r8nvmz8tAhET7zDASI',
 | 
				
			||||||
 | 
					            url: "https://openstreetmap.org"
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        "osm-test": {
 | 
				
			||||||
 | 
					            oauth_consumer_key: 'Zgr7EoKb93uwPv2EOFkIlf3n9NLwj5wbyfjZMhz2',
 | 
				
			||||||
 | 
					            oauth_secret: '3am1i1sykHDMZ66SGq4wI2Z7cJMKgzneCHp3nctn',
 | 
				
			||||||
 | 
					            url: "https://master.apis.dev.openstreetmap.org"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    public auth;
 | 
					    public auth;
 | 
				
			||||||
    public userDetails: UIEventSource<UserDetails>;
 | 
					    public userDetails: UIEventSource<UserDetails>;
 | 
				
			||||||
    _dryRun: boolean;
 | 
					    _dryRun: boolean;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    public preferencesHandler: OsmPreferences;
 | 
					    public preferencesHandler: OsmPreferences;
 | 
				
			||||||
    public changesetHandler: ChangesetHandler;
 | 
					    public changesetHandler: ChangesetHandler;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    private _onLoggedIn: ((userDetails: UserDetails) => void)[] = [];
 | 
					    private _onLoggedIn: ((userDetails: UserDetails) => void)[] = [];
 | 
				
			||||||
    private readonly _iframeMode: Boolean | boolean;
 | 
					    private readonly _iframeMode: Boolean | boolean;
 | 
				
			||||||
    private readonly _singlePage: boolean;
 | 
					    private readonly _singlePage: boolean;
 | 
				
			||||||
 | 
					    private readonly _oauth_config: {
 | 
				
			||||||
 | 
					        oauth_consumer_key: string,
 | 
				
			||||||
 | 
					        oauth_secret: string,
 | 
				
			||||||
 | 
					        url: string
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor(dryRun: boolean, oauth_token: UIEventSource<string>,
 | 
					    constructor(dryRun: boolean, oauth_token: UIEventSource<string>,
 | 
				
			||||||
                // Used to keep multiple changesets open and to write to the correct changeset
 | 
					                // Used to keep multiple changesets open and to write to the correct changeset
 | 
				
			||||||
                layoutName: string,
 | 
					                layoutName: string,
 | 
				
			||||||
                singlePage: boolean = true) {
 | 
					                singlePage: boolean = true,
 | 
				
			||||||
 | 
					                osmConfiguration: "osm" | "osm-test" = 'osm'
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
        this._singlePage = singlePage;
 | 
					        this._singlePage = singlePage;
 | 
				
			||||||
        this._iframeMode = window !== window.top;
 | 
					        this._oauth_config = OsmConnection._oauth_configs[osmConfiguration] ?? OsmConnection._oauth_configs.osm;
 | 
				
			||||||
 | 
					        console.debug("Using backend", this._oauth_config.url)
 | 
				
			||||||
 | 
					        this._iframeMode = Utils.runningFromConsole ? false : window !== window.top;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        this.userDetails = new UIEventSource<UserDetails>(new UserDetails(), "userDetails");
 | 
					        this.userDetails = new UIEventSource<UserDetails>(new UserDetails(), "userDetails");
 | 
				
			||||||
        this.userDetails.data.dryRun = dryRun;
 | 
					        this.userDetails.data.dryRun = dryRun;
 | 
				
			||||||
| 
						 | 
					@ -68,37 +90,6 @@ export class OsmConnection {
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private updateAuthObject(){
 | 
					 | 
				
			||||||
        let pwaStandAloneMode = false;
 | 
					 | 
				
			||||||
        try {
 | 
					 | 
				
			||||||
            if (window.matchMedia('(display-mode: standalone)').matches || window.matchMedia('(display-mode: fullscreen)').matches) {
 | 
					 | 
				
			||||||
                pwaStandAloneMode = true;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        } catch (e) {
 | 
					 | 
				
			||||||
            console.warn("Detecting standalone mode failed", e, ". Assuming in browser and not worrying furhter")
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        if (this._iframeMode || pwaStandAloneMode || !this._singlePage) {
 | 
					 | 
				
			||||||
            // In standalone mode, we DON'T use single page login, as 'redirecting' opens a new window anyway...
 | 
					 | 
				
			||||||
            // Same for an iframe...
 | 
					 | 
				
			||||||
            this.auth = new osmAuth({
 | 
					 | 
				
			||||||
                oauth_consumer_key: 'hivV7ec2o49Two8g9h8Is1VIiVOgxQ1iYexCbvem',
 | 
					 | 
				
			||||||
                oauth_secret: 'wDBRTCem0vxD7txrg1y6p5r8nvmz8tAhET7zDASI',
 | 
					 | 
				
			||||||
                singlepage: false,
 | 
					 | 
				
			||||||
                auto: true,
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            this.auth = new osmAuth({
 | 
					 | 
				
			||||||
                oauth_consumer_key: 'hivV7ec2o49Two8g9h8Is1VIiVOgxQ1iYexCbvem',
 | 
					 | 
				
			||||||
                oauth_secret: 'wDBRTCem0vxD7txrg1y6p5r8nvmz8tAhET7zDASI',
 | 
					 | 
				
			||||||
                singlepage: true,
 | 
					 | 
				
			||||||
                landing: window.location.href,
 | 
					 | 
				
			||||||
                auto: true,
 | 
					 | 
				
			||||||
            });
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    public UploadChangeset(
 | 
					    public UploadChangeset(
 | 
				
			||||||
        layout: LayoutConfig,
 | 
					        layout: LayoutConfig,
 | 
				
			||||||
        allElements: ElementStorage,
 | 
					        allElements: ElementStorage,
 | 
				
			||||||
| 
						 | 
					@ -196,6 +187,33 @@ export class OsmConnection {
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private updateAuthObject() {
 | 
				
			||||||
 | 
					        let pwaStandAloneMode = false;
 | 
				
			||||||
 | 
					        try {
 | 
				
			||||||
 | 
					            if (Utils.runningFromConsole) {
 | 
				
			||||||
 | 
					                pwaStandAloneMode = true
 | 
				
			||||||
 | 
					            } else if (window.matchMedia('(display-mode: standalone)').matches || window.matchMedia('(display-mode: fullscreen)').matches) {
 | 
				
			||||||
 | 
					                pwaStandAloneMode = true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        } catch (e) {
 | 
				
			||||||
 | 
					            console.warn("Detecting standalone mode failed", e, ". Assuming in browser and not worrying furhter")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const standalone = this._iframeMode || pwaStandAloneMode || !this._singlePage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // In standalone mode, we DON'T use single page login, as 'redirecting' opens a new window anyway...
 | 
				
			||||||
 | 
					        // Same for an iframe...
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        this.auth = new osmAuth({
 | 
				
			||||||
 | 
					            oauth_consumer_key: this._oauth_config.oauth_consumer_key,
 | 
				
			||||||
 | 
					            oauth_secret: this._oauth_config.oauth_secret,
 | 
				
			||||||
 | 
					            url: this._oauth_config.url,
 | 
				
			||||||
 | 
					            landing: standalone ? undefined : window.location.href,
 | 
				
			||||||
 | 
					            singlepage: !standalone,
 | 
				
			||||||
 | 
					            auto: true,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private CheckForMessagesContinuously() {
 | 
					    private CheckForMessagesContinuously() {
 | 
				
			||||||
        const self = this;
 | 
					        const self = this;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -96,10 +96,13 @@ export class OsmPreferences {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public GetPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {
 | 
					    public GetPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {
 | 
				
			||||||
 | 
					        console.warn(key)
 | 
				
			||||||
        key = prefix + key;
 | 
					        key = prefix + key;
 | 
				
			||||||
 | 
					        key = key.replace(/[:\\\/"' {}.%]/g, '')
 | 
				
			||||||
        if (key.length >= 255) {
 | 
					        if (key.length >= 255) {
 | 
				
			||||||
            throw "Preferences: key length to big";
 | 
					            throw "Preferences: key length to big";
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        console.warn(key)
 | 
				
			||||||
        if (this.preferenceSources[key] !== undefined) {
 | 
					        if (this.preferenceSources[key] !== undefined) {
 | 
				
			||||||
            return this.preferenceSources[key];
 | 
					            return this.preferenceSources[key];
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										10
									
								
								State.ts
									
										
									
									
									
								
							
							
						
						
									
										10
									
								
								State.ts
									
										
									
									
									
								
							| 
						 | 
					@ -98,6 +98,7 @@ export default class State {
 | 
				
			||||||
    public readonly featureSwitchIsTesting: UIEventSource<boolean>;
 | 
					    public readonly featureSwitchIsTesting: UIEventSource<boolean>;
 | 
				
			||||||
    public readonly featureSwitchIsDebugging: UIEventSource<boolean>;
 | 
					    public readonly featureSwitchIsDebugging: UIEventSource<boolean>;
 | 
				
			||||||
    public readonly featureSwitchShowAllQuestions: UIEventSource<boolean>;
 | 
					    public readonly featureSwitchShowAllQuestions: UIEventSource<boolean>;
 | 
				
			||||||
 | 
					    public readonly featureSwitchApiURL: UIEventSource<string>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /**
 | 
					    /**
 | 
				
			||||||
| 
						 | 
					@ -201,7 +202,6 @@ export default class State {
 | 
				
			||||||
            this.featureSwitchShowAllQuestions = featSw("fs-all-questions", (layoutToUse) => layoutToUse?.enableShowAllQuestions ?? false,
 | 
					            this.featureSwitchShowAllQuestions = featSw("fs-all-questions", (layoutToUse) => layoutToUse?.enableShowAllQuestions ?? false,
 | 
				
			||||||
                "Always show all questions");
 | 
					                "Always show all questions");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
            this.featureSwitchIsTesting = QueryParameters.GetQueryParameter("test", "false",
 | 
					            this.featureSwitchIsTesting = QueryParameters.GetQueryParameter("test", "false",
 | 
				
			||||||
                "If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org")
 | 
					                "If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org")
 | 
				
			||||||
                .map(str => str === "true", [], b => "" + b);
 | 
					                .map(str => str === "true", [], b => "" + b);
 | 
				
			||||||
| 
						 | 
					@ -209,6 +209,10 @@ export default class State {
 | 
				
			||||||
            this.featureSwitchIsDebugging = QueryParameters.GetQueryParameter("debug","false",
 | 
					            this.featureSwitchIsDebugging = QueryParameters.GetQueryParameter("debug","false",
 | 
				
			||||||
                "If true, shows some extra debugging help such as all the available tags on every object")
 | 
					                "If true, shows some extra debugging help such as all the available tags on every object")
 | 
				
			||||||
                .map(str => str === "true", [], b => "" + b)
 | 
					                .map(str => str === "true", [], b => "" + b)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            this.featureSwitchApiURL = QueryParameters.GetQueryParameter("backend","https://openstreetmap.org",
 | 
				
			||||||
 | 
					                "The OSM backend to use - can be used to redirect mapcomplete to a testing backend or e.g. openHistoricalMap")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -217,7 +221,9 @@ export default class State {
 | 
				
			||||||
            QueryParameters.GetQueryParameter("oauth_token", undefined,
 | 
					            QueryParameters.GetQueryParameter("oauth_token", undefined,
 | 
				
			||||||
                "Used to complete the login"),
 | 
					                "Used to complete the login"),
 | 
				
			||||||
            layoutToUse?.id,
 | 
					            layoutToUse?.id,
 | 
				
			||||||
            true
 | 
					            true,
 | 
				
			||||||
 | 
					            // @ts-ignore
 | 
				
			||||||
 | 
					            this.featureSwitchApiURL.data
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										3
									
								
								test.ts
									
										
									
									
									
								
							
							
						
						
									
										3
									
								
								test.ts
									
										
									
									
									
								
							| 
						 | 
					@ -1,3 +1,4 @@
 | 
				
			||||||
import ValidatedTextField from "./UI/Input/ValidatedTextField";
 | 
					import ValidatedTextField from "./UI/Input/ValidatedTextField";
 | 
				
			||||||
 | 
					import TestAll from "./test/TestAll";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ValidatedTextField.InputForType("phone").AttachTo("maindiv")
 | 
					new TestAll().testAll();
 | 
				
			||||||
							
								
								
									
										46
									
								
								test/OsmConnection.spec.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								test/OsmConnection.spec.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,46 @@
 | 
				
			||||||
 | 
					import T from "./TestHelper";
 | 
				
			||||||
 | 
					import UserDetails, {OsmConnection} from "../Logic/Osm/OsmConnection";
 | 
				
			||||||
 | 
					import {UIEventSource} from "../Logic/UIEventSource";
 | 
				
			||||||
 | 
					import ScriptUtils from "../scripts/ScriptUtils";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class OsmConnectionSpec extends T {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /*
 | 
				
			||||||
 | 
					    This token gives access to the TESTING-instance of OSM. No real harm can be done with it, so it can be commited to the repo
 | 
				
			||||||
 | 
					     */
 | 
				
			||||||
 | 
					    private static _osm_token = "LJFmv2nUicSNmBNsFeyCHx5KKx6Aiesx8pXPbX4n"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    constructor() {
 | 
				
			||||||
 | 
					        super("OsmConnectionSpec-test", [
 | 
				
			||||||
 | 
					            ["login on dev",
 | 
				
			||||||
 | 
					                () => {
 | 
				
			||||||
 | 
					                   const osmConn = new OsmConnection(false,
 | 
				
			||||||
 | 
					                        new UIEventSource<string>(undefined),
 | 
				
			||||||
 | 
					                        "Unit test",
 | 
				
			||||||
 | 
					                        true,
 | 
				
			||||||
 | 
					                        "osm-test"
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    osmConn.userDetails.map((userdetails : UserDetails) => {
 | 
				
			||||||
 | 
					                       if(userdetails.loggedIn){
 | 
				
			||||||
 | 
					                           console.log("Logged in with the testing account. Writing some random data to test preferences")
 | 
				
			||||||
 | 
					                           const data = Math.random().toString()
 | 
				
			||||||
 | 
					                           osmConn.GetPreference("test").setData(data)
 | 
				
			||||||
 | 
					                           
 | 
				
			||||||
 | 
					                           osmConn.GetPreference("https://raw.githubusercontent.com/AgusQui/MapCompleteRailway/main/railway")
 | 
				
			||||||
 | 
					                               .setData(data)
 | 
				
			||||||
 | 
					                           
 | 
				
			||||||
 | 
					                       }
 | 
				
			||||||
 | 
					                    });
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    ScriptUtils.sleep(1000)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ]);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -9,7 +9,26 @@ import TagQuestionSpec from "./TagQuestion.spec";
 | 
				
			||||||
import ImageSearcherSpec from "./ImageSearcher.spec";
 | 
					import ImageSearcherSpec from "./ImageSearcher.spec";
 | 
				
			||||||
import ThemeSpec from "./Theme.spec";
 | 
					import ThemeSpec from "./Theme.spec";
 | 
				
			||||||
import UtilsSpec from "./Utils.spec";
 | 
					import UtilsSpec from "./Utils.spec";
 | 
				
			||||||
 | 
					import OsmConnectionSpec from "./OsmConnection.spec";
 | 
				
			||||||
 | 
					import T from "./TestHelper";
 | 
				
			||||||
 | 
					import {FixedUiElement} from "../UI/Base/FixedUiElement";
 | 
				
			||||||
 | 
					import Combine from "../UI/Base/Combine";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default class TestAll {
 | 
				
			||||||
 | 
					    private needsBrowserTests: T[] = [new OsmConnectionSpec()]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public testAll(): void {
 | 
				
			||||||
 | 
					        Utils.runningFromConsole = false
 | 
				
			||||||
 | 
					        for (const test of this.needsBrowserTests.concat(allTests)) {
 | 
				
			||||||
 | 
					            if (test.failures.length > 0) {
 | 
				
			||||||
 | 
					                new Combine([new FixedUiElement("TEST FAILED: " + test.name).SetStyle("background: red"),
 | 
				
			||||||
 | 
					                    ...test.failures])
 | 
				
			||||||
 | 
					                    .AttachTo("maindiv")
 | 
				
			||||||
 | 
					                throw "Some test failed"
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const allTests = [
 | 
					const allTests = [
 | 
				
			||||||
    new TagSpec(),
 | 
					    new TagSpec(),
 | 
				
			||||||
| 
						 | 
					@ -20,8 +39,9 @@ const allTests = [
 | 
				
			||||||
    new ThemeSpec(),
 | 
					    new ThemeSpec(),
 | 
				
			||||||
    new UtilsSpec()]
 | 
					    new UtilsSpec()]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
for (const test of allTests) {
 | 
					for (const test of allTests) {
 | 
				
			||||||
    if(test.failures.length > 0){
 | 
					    if (test.failures.length > 0) {
 | 
				
			||||||
        throw "Some test failed"
 | 
					        throw "Some test failed"
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,10 @@
 | 
				
			||||||
export default class T {
 | 
					export default class T {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public readonly failures = []
 | 
					    public readonly failures : string[] = []
 | 
				
			||||||
 | 
					    public readonly name : string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    constructor(testsuite: string, tests: [string, () => void][]) {
 | 
					    constructor(testsuite: string, tests: [string, () => void][]) {
 | 
				
			||||||
 | 
					        this.name = testsuite
 | 
				
			||||||
        for (const [name, test] of tests) {
 | 
					        for (const [name, test] of tests) {
 | 
				
			||||||
            try {
 | 
					            try {
 | 
				
			||||||
                test();
 | 
					                test();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue