繁体   English   中英

Appium - 如何在iOS设备上设置地理位置?

[英]Appium - How to set Geo Location on iOS Device?

Meta: -

  • iOS模拟器设备v10.3
  • Appium java-client v5.0.0 BETA8
  • Selenium v​​3.4.0

实际上我正在尝试使用Appium XCUITest自动化在iOS设备中设置GeoLocation 我试过使用下面的代码,这在Android设备上正常工作,而在iOS上抛出异常:

import org.openqa.selenium.html5.Location;

AppiumServiceBuilder builder = new AppiumServiceBuilder().usingAnyFreePort().withAppiumJS("path/to/appium/main.js");

DesiredCapabilities capabilities = new DesiredCapabilities();
capabilities.setCapability("automationName", "XCUITest");
IOSDriver driver= new IOSDriver(builder, capabilities);

//Here this code working fine with AndroidDriver
Location location = new Location(latitude, longitude, altitude);    
driver.setLocation(location);

例外:

org.openqa.selenium.WebDriverException:方法尚未实现(警告:服务器未提供任何堆栈跟踪信息)

当我尝试使用JavascriptExecutor时:

Map<String, String> args = new HashMap<String, String>();
args.put("address", "Address");
((JavascriptExecutor)webDriver).executeScript("mobile:setLocation", args);

例外:

org.openqa.selenium.UnsupportedCommandException:未知的移动命令“setLocation”。 只有滚动,滑动,捏合,双击,两个手指点击,触摸和保持,点击,拖动来自动态,selectPickerWheelValue,才支持警报命令。 (警告:服务器未提供任何堆栈跟踪信息)

当我尝试:

import org.openqa.selenium.remote.DriverCommand;

Map<String, String> args = new HashMap<String, String>();
args.put("location", "Address");
driver.execute(DriverCommand.SET_LOCATION, args);

例外:

org.openqa.selenium.WebDriverException:方法尚未实现(警告:服务器未提供任何堆栈跟踪信息)

无论如何使用appium在iOS上设置GeoLocation

Appium日志:

[debug] [JSONWP Proxy]获得状态为200的响应:“{\\ n \\”value \\“:{\\ n \\”state \\“:\\”success \\“,\\ n \\”os \\“:{\\ n \\ “name \\”:\\“iOS \\”,\\ n \\“version \\”:\\“10.3.1 \\”\\ n},\\ n \\“ios \\”:{\\ n \\“simulatorVersion \\”:\\“10.3 .1 \\“,\\ n \\”ip \\“:\\”192.168.1.17 \\“\\ n},\\ n \\”build \\“:{\\ n \\”time \\“:\\”2017年8月29日15:40: 09 \\“\\ n} \\ n},\\ n \\”sessionId \\“:\\”10A97A93-D13A-4888-A536-0D62E0674A2B \\“,\\ n \\”状态\\“:0 \\ n}”

[debug] [XCUITest] WebDriverAgent在ip上运行'192.168.1.17'[调试] [XCUITest] WebDriverAgent在16121ms后成功启动[debug] [BaseDriver]事件'wdaSessionAttempted'登录于1504013035278(格林尼治标准时间18:53:55 + 0530(IST) ))[debug] [XCUITest]将createSession命令发送到WDA [debug] [JSONWP Proxy]将[POST / session]代理到[POST http:// localhost:8100 / session] with body:{“desiredCapabilities”:{“bundleId “:” com.example.apple-samplecode.UICatalog”, “参数”:[], “环境”:{}, “shouldWaitForQuiescence”:真 “shouldUseTestManagerForVisibilityDetection”:假 “maxTypingFrequency”:120, “shouldUseSingletonTestManager”: true}} [debug] [JSONWP Proxy]得到状态为200的响应:{“value”:{“sessionId”:“43710C7E-2FDE-4A35-A2E0-4D309EE2CE9C”,“capabilities”:{“device”:“iphone” “browserName”: “UICatalog”, “sdkVersion”: “10.3.1”, “CFBundleIdentifier”: “com.example.apple-samplecode.UICatalog”}}, “的sessionId”:“43710C7E-2FDE-4A35-A2E0- 4D309EE2CE9C“,”status“:0} [debug] [BaseDriver]事件'wdaSessio nStarted”记录在1504013038184(18时53分58秒GMT + 0530(IST))[调试] [XCUITest]找到WDA导出的数据文件夹: '/Users/omprakash.mishra/Library/Developer/Xcode/DerivedData/WebDriverAgent-dikkwtrisltbeobjmfvpthwwekvs' [XCUITest]设置 '555' 权限 '/Users/omprakash.mishra/Library/Developer/Xcode/DerivedData/WebDriverAgent-dikkwtrisltbeobjmfvpthwwekvs/Logs/Test/Attachments' 文件夹[调试] [XCUITest]找到WDA导出的数据文件夹中:“/ Users / omprakash.mishra / Library / Developer / Xcode / DerivedData / WebDriverAgent-folfazwwukpzfkegdblpnfuwlvfn'[XCUITest]将'555'权限设置为'/Users/omprakash.mishra/Library/Developer/Xcode/DerivedData/WebDriverAgent-folfazwwukpzfkegdblpnfuwlvfn/Logs/Test / Attachments'文件夹[debug] [BaseDriver]事件'wdaPermsAdjusted'记录于1504013038192(格林威治标准时间18:53:58(IST))[调试] [BaseDriver]事件'wdaStarted'记录于1504013038193(格林威治标准时间18:53:58) +0530(IST))[debug] [XCUITest]将初始方向设置为'PORTRAIT'[debug] [JSONWP Proxy]代理 [POST / orientation]到[POST http:// localhost:8100 / session / 43710C7E-2FDE-4A35-A2E0-4D309EE2CE9C / orientation] with body:{“orientation”:“PORTRAIT”} [debug] [JSONWP Proxy] Got状态为200的响应:{“value”:{},“sessionId”:“43710C7E-2FDE-4A35-A2E0-4D309EE2CE9C”,“status”:0} [debug] [BaseDriver]事件'orientationSet'记录于1504013038453(18 :格林威治标准时间53:58(IST))[Appium]新的XCUITestDriver会话成功创建,会话6909c363-12a5-4a21-9298-c7f750ba7e09添加到主会话列表[debug] [BaseDriver]事件'newSessionStarted'记录于1504013038456(18 :53:58 GMT + 0530(IST))[debug] [MJSONWP]使用driver.createSession()结果响应客户端:{“webStorageEnabled”:false,“locationContextEnabled”:false,“browserName”:“”,“platform “:” MAC “ ”javascriptEnabled“:真 ”databaseEnabled“:假 ”takesScreenshot“:真 ”networkConnectionEnabled“:假, ”应用程序“: ”SRC /测试/资源/执行/ UICatalog.app“,” maxTypingFrequency “:” 120" , “newCommandTimeout”:0 “platformVersion”: “10.3”,“自动 mationName“:”XCUITest“,”platformName“:”iOS“,”udid“:”0A41ECE4-6D03-4FEA-A82A-858FDBA6620E“,”deviceName“:”iPhone 6“} [HTTP] < - POST / wd / hub / session 200 46915 ms - 512 [HTTP] - > GET / wd / hub / session / 6909c363-12a5-4a21-9298-c7f750ba7e09 {} [debug] [MJSONWP]用args调用AppiumDriver.getSession():[“ 6909c363-12a5-4a21-9298-c7f750ba7e09“] [debug] [XCUITest]执行命令'getSession'[debug] [JSONWP Proxy]将[GET /]代理为[GET http:// localhost:8100 / session / 43710C7E-2FDE没有主体[ -4A35-A2E0-4D309EE2CE9C] [调试] [JSONWP代理]获得状态200的响应:“{\\ n \\”值\\“:{\\ n \\”sessionId \\“:\\”43710C7E-2FDE-4A35- A2E0-4D309EE2CE9C \\“,\\ n \\”capabilities \\“:{\\ n \\”device \\“:\\”iphone \\“,\\ n \\”browserName \\“:\\”UICatalog \\“,\\ n
\\“sdkVersion \\”:\\“10.3.1 \\”,\\ n \\“CFBundleIdentifier \\”:\\“com.example.apple-samplecode.UICatalog \\”\\ n} \\ n},\\ n \\“sessionId \\”: \\“43710C7E-2FDE-4A35-A2E0-4D309EE2CE9C \\”,\\ n \\“状态\\”:0 \\ n}“[XCUITest]合并用于会话详细信息响应的Appium上限的WDA上限[debug] [MJSONWP]响应客户端driver.getSession()result:{“udid”:“”,“app”:“src / test / resources / executor / UICatalog.app”,“maxTypingFrequency”:120,“newCommandTimeout”:0,“platformVersion”:“ 10.3“,”automationName“:”XCUITest“,”platformName“:”iOS“,”deviceName“:”iPhone 6“,”device“:”iphone“,”browserName“:”UICatalog“,”sdkVersion“:”10.3 .1“,”CFBundleIdentifier“:”com.example.apple-samplecode.UICatalog“} [HTTP] < - GET / wd / hub / session / 6909c363-12a5-4a21-9298-c7f750ba7e09 200 110 ms - 406 [HTTP ] - > GET / wd / hub / session / 6909c363-12a5-4a21-9298-c7f750ba7e09 {} [debug] [MJSONWP]用args调用AppiumDriver.getSession():[“6909c363-12a5-4a21-9298-c7f750ba7e09” ] [debug] [XCUITest]执行命令'getSession'[debug] [JSONWP Proxy ] [GET /]代表[GET http:// localhost:8100 / session / 43710C7E-2FDE-4A35-A2E0-4D309EE2CE9C]没有正文[debug] [JSONWP代理]得到状态为200的响应:“{\\ n \\ “value \\”:{\\ n \\“sessionId \\”:\\“43710C7E-2FDE-4A35-A2E0-4D309EE2CE9C \\”,\\ n \\“capabilities \\”:{\\ n \\“device \\”:\\“iphone \\” ,\\ n \\“browserName \\”:\\“UICatalog \\”,\\ n
\\“sdkVersion \\”:\\“10.3.1 \\”,\\ n \\“CFBundleIdentifier \\”:\\“com.example.apple-samplecode.UICatalog \\”\\ n} \\ n},\\ n \\“sessionId \\”: \\“43710C7E-2FDE-4A35-A2E0-4D309EE2CE9C \\”,\\ n \\“状态\\”:0 \\ n}“[XCUITest]合并用于会话详细信息响应的Appium上限的WDA上限[debug] [MJSONWP]响应客户端driver.getSession()result:{“udid”:“”,“app”:“src / test / resources / executor / UICatalog.app”,“maxTypingFrequency”:120,“newCommandTimeout”:0,“platformVersion”:“ 10.3“,”automationName“:”XCUITest“,”platformName“:”iOS“,”deviceName“:”iPhone 6“,”device“:”iphone“,”browserName“:”UICatalog“,”sdkVersion“:”10.3 .1“,”CFBundleIdentifier“:”com.example.apple-samplecode.UICatalog“} [HTTP] < - GET / wd / hub / session / 6909c363-12a5-4a21-9298-c7f750ba7e09 200 103 ms - 406 [HTTP ] - > POST / wd / hub / session / 6909c363-12a5-4a21-9298-c7f750ba7e09 / location {“location”:{“altitude”:0,“latitude”:20.672267,“hCode”:1751403001,“class” :“org.openqa.selenium.html5.Location”,“经度”:83.1649}} [debug] [MJSONWP]调用AppiumDri ver.setGeoLocation()with args:[{“altitude”:0,“latitude”:20.672267,“hCode”:1751403001,“class”:“org.openqa.selenium.html5.Location”,“longitude”:83.1649} ,“6909c363-12a5-4a21-9298-c7f750ba7e09”] [debug] [XCUITest]执行命令'setGeoLocation'[HTTP] < - POST / wd / hub / session / 6909c363-12a5-4a21-9298-c7f750ba7e09 / location 501 30 ms - 122 org.openqa.selenium.WebDriverException:方法尚未实现(警告:服务器未提供任何堆栈跟踪信息)命令持续时间或超时:58毫秒构建信息:版本:'3.4.0',修订版: '未知',时间:'未知'系统信息:主持人:'Abhays-MacBook-Air.local',ip:'fe80:0:0:0:4fc:aa3c:d673:369e%en0',os.name: 'Mac OS X',os.arch:'x86_64',os.version:'10 .12.5'​​,java.version:'1.8.0_131'驱动程序信息:io.appium.java_client.ios.IOSDriver功能[{app = src / test / resources / executor / UICatalog.app,networkConnectionEnabled = false,databaseEnabled = false,deviceName = iPhone 6,platform = MAC,maxTypingFrequency = 120,newC ommandTimeout = 0,platformVersion = 10.3,webStorageEnabled = false,locationContextEnabled = false,automationName = XCUITest,browserName =,takesScreenshot = true,javascriptEnabled = true,platformName = iOS,udid = 0A41ECE4-6D03-4FEA-A82A-858FDBA6620E}]会话ID :6909c363-12a5-4a21-9298-c7f750ba7e09在sun.reflect.NativeConstructorAccessorImpl.newInstance0(本机方法)在sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)在sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java: 45)atg.openqa.selenium.remote上的org.openqa.selenium.remote.ErrorHandler.createThrowable(ErrorHandler.java:215)中的java.lang.reflect.Constructor.newInstance(Constructor.java:423),org.openqa.selenium.remote.ErrorHandler.throwIfResponseFailed (ErrorHandler.java:167)在org.openqa.selenium.remote.RemoteWebDriver.execute(RemoteWebDriver.java:671)在io.appium.java_client.DefaultGenericMobileDriver.execute(DefaultGenericMobileDriver.java:42)在io.appium.java_client .AppiumDriver.execute(AppiumDriver.java:1)ato.appium.java_client.ios.IOSDriver.execute(IOSDriver.java:1)ato.appium.java_client.AppiumExecutionMethod.execute(AppiumExecutionMethod.java:46)at org。在org.openqa.selenium.html5.LocationContext $ setLocation.call的io.appium.java_client.AppiumDriver.setLocation(AppiumDriver.java:400)中的openqa.selenium.remote.html5.RemoteLocationContext.setLocation(RemoteLocationContext.java:50)来自org.codehaus的org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:45)的org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:110)。位于sun.reflect.NativeMethodAccessorImpl.invoke0(native方法)的executor.com.bqurious.keyword.mobile.ios.BqIosSetLocationTest.setLocation(BqIosSetLocationTest.groovy:72)中的groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:122) )在sun.reflect.DelegatingMethodAcc的sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) essorImpl.invoke(DelegatingMethodAccessorImpl.java:43)位于org.junit.runners.model.FrameworkMethod $ 1.runReflectiveCall(FrameworkMethod.java:50)的java.lang.reflect.Method.invoke(Method.java:498)。 junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)org.junit.internal.runners.statements.InvokeMethod。在org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)在org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:78)在org.junit.runners.BlockJUnit4ClassRunner评估(InvokeMethod.java:17) .runChild(BlockJUnit4ClassRunner.java:57)位于org.junit的org.junit.runners.ParentRunner $ 3.run(ParentRunner.java:290)org.junit.runners.ParentRunner $ 1.schedule(ParentRunner.java:71)。 runners.ParentRunner.runChildren(ParentRunner.java:288)org.junit.runners.ParentRunner.access $ 000(ParentRunner.java:58)org.junit.runners.ParentRunner $ 2.evaluate(P arentRunner.java:268)org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)org.junit.internal.runners.statements.RunAfters.evaluate(RunAfters.java:27)at org org.eclipse.jdt.internal.junit.runner上的org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)中的.junit.runners.ParentRunner.run(ParentRunner.java:363) .TestExecution.run(TestExecution.java:38)在org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:459)在org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests (RemoteTestRunner.java:675)org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:382)org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java) :192)[HTTP] - > DELETE / wd / hub / session / 6909c363-12a5-4a21-9298-c7f750ba7e09 {} [debug] [MJSONWP]用args调用AppiumDriver.deleteSession():[“6909c363-12a5-4a21 -9298-c7f750ba7e09“] [debug] [BaseDriver]事件'退出 SessionRequested'登录于1504013038955(格林威治标准时间18:53:58 + 0530(IST))[调试] [JSONWP代理]将[DELETE / session / 6909c363-12a5-4a21-9298-c7f750ba7e09]代理为[DELETE http:// localhost:没有主体的8100 / session / 43710C7E-2FDE-4A35-A2E0-4D309EE2CE9C] [调试] [JSONWP代理]得到状态为200的响应:“{\\ n \\”值\\“:{\\ n \\ n},\\ n \\ “sessionId \\”:\\“28E97E0B-DF47-4325-8991-A28B77134EDB \\”,\\ n \\“status \\”:0 \\ n}“[XCUITest]关闭子进程[XCUITest]关闭xcodebuild进程(pid 37304 '/Users/omprakash.mishra/Library/Developer/Xcode/DerivedData/WebDriverAgent-dikkwtrisltbeobjmfvpthwwekvs'[XCUITest:)[XCUITest] xcodebuild联编用码 '空' 和信号 'SIGTERM'[调试] [XCUITest]找到WDA导出的数据文件夹中退出将'755'权限设置为'/Users/omprakash.mishra/Library/Developer/Xcode/DerivedData/WebDriverAgent-dikkwtrisltbeobjmfvpthwwekvs/Logs/Test/Attachments'文件夹[debug] [XCUITest]找到WDA派生数据文件夹:'/ Users / omprakash.mishra /库/开发商/ Xcode中/ d erivedData / WebDriverAgent-folfazwwukpzfkegdblpnfuwlvfn'[XCUITest]将'755'权限设置为'/Users/omprakash.mishra/Library/Developer/Xcode/DerivedData/WebDriverAgent-folfazwwukpzfkegdblpnfuwlvfn/Logs/Test/Attachments'文件夹[debug] [XCUITest]不清除日志文件。 使用clearSystemFiles功能打开。 [debug] [iOSLog]停止iOS日志捕获[Appium]从我们的主会话列表中删除会话6909c363-12a5-4a21-9298-c7f750ba7e09 [调试] [BaseDriver]事件'quitSessionFinished'记录在1504013039408(格林尼治标准时间18:53:59) 0530(IST))[debug] [MJSONWP]收到响应:null [debug] [MJSONWP]但是删除会话,所以不返回[debug] [MJSONWP]用driver.deleteSession()结果响应客户端结果:null [HTTP] < - DELETE / wd / hub / session / 6909c363-12a5-4a21-9298-c7f750ba7e09 200 461 ms - 76

这个AppleScript会起作用。

public static void setLocation(Location loc) {
    try {
        String[] cmd = {"osascript", "-e",
                "on menu_click(mList)\n" +
                        "    local appName, topMenu, r\n" +
                        "\n" +
                        "    -- Validate our input\n" +
                        "    if mList's length < 3 then error \"Menu list is not long enough\"\n" +
                        "\n" +
                        "    -- Set these variables for clarity and brevity later on\n" +
                        "    set {appName, topMenu} to (items 1 through 2 of mList)\n" +
                        "    set r to (items 3 through (mList's length) of mList)\n" +
                        "\n" +
                        "    -- This overly-long line calls the menu_recurse function with\n" +
                        "    -- two arguments: r, and a reference to the top-level menu\n" +
                        "    tell application \"System Events\" to my menu_click_recurse(r, ((process appName)'s ¬\n" +
                        "        (menu bar 1)'s (menu bar item topMenu)'s (menu topMenu)))\n" +
                        "end menu_click\n" +
                        "\n" +
                        "on menu_click_recurse(mList, parentObject)\n" +
                        "    local f, r\n" +
                        "\n" +
                        "    -- `f` = first item, `r` = rest of items\n" +
                        "    set f to item 1 of mList\n" +
                        "    if mList's length > 1 then set r to (items 2 through (mList's length) of mList)\n" +
                        "\n" +
                        "    -- either actually click the menu item, or recurse again\n" +
                        "    tell application \"System Events\"\n" +
                        "        if mList's length is 1 then\n" +
                        "            click parentObject's menu item f\n" +
                        "        else\n" +
                        "            my menu_click_recurse(r, (parentObject's (menu item f)'s (menu f)))\n" +
                        "        end if\n" +
                        "    end tell\n" +
                        "end menu_click_recurse\n" +
                        "\n" +
                        "application \""+simulatorAppName()+"\" activate    \n" +
                        "delay 0.2\n" +
                        "menu_click({\""+simulatorAppName()+"\",\"Debug\", \"Location\", \"None\"})\n" +
                        "\n" +
                        "delay 0.2\n" +
                        "menu_click({\""+simulatorAppName()+"\",\"Debug\", \"Location\", \"Custom Location…\"})\n" +
                        "\n" +
                        "delay 0.2\n" +
                        "tell application \"System Events\"\n" +
                        "    tell process \""+simulatorAppName()+"\"\n" +
                        "        set value of text field 1 of window \"Custom Location\" to \""+loc.getLatitude()+"\"\n" +
                        "        set value of text field 2 of window \"Custom Location\" to \""+loc.getLongitude()+"\"\n" +
                        "        click UI Element \"OK\" of window \"Custom Location\"\n" +
                        "    end tell\n" +
                        "end tell"
        };
        Process process = Runtime.getRuntime().exec(cmd);
        BufferedReader bufferedReader = new BufferedReader(
                new InputStreamReader(process.getErrorStream()));
        String lsString;
        while ((lsString = bufferedReader.readLine()) != null) {
            System.out.println(lsString);
        }
        try{Thread.sleep(10000);}catch (Exception e1){}
    } catch (Exception e) {}
}

public static String simulatorAppName() {
    return "Simulator";
}

看来你需要一个AppiumDriver。 根据这个评论。

您可以使用setLocation方法在Android模拟器或iOS模拟器中设置纬度和经度:

import org.openqa.selenium.html5.Location;

位置loc = new Location(20.0,12.5,1000); //纬度,经度,高度driver.setLocation(loc);

我同意使用Apple脚本为iOS模拟器设置自定义地理位置的解决方案,因为Appium不支持iOS的这种方法(Apple没有为XCTest框架提供模拟GPS位置的API)

Apple脚本:

#!/usr/bin/env bash

osascript -e 'tell application "System Events"
    tell process "Simulator"
        set frontmost to true
        click menu item "Custom Location…" of menu of menu item "Location" of menu "Debug" of menu bar 1
        set popup to window "Custom Location"
        set value of text field 1 of popup to (system attribute "Latitude")
        set value of text field 2 of popup to (system attribute "Longitude")
        click button "OK" of popup
    end tell
end tell'

我正在使用python,所以这里是我的Android和iOS解决方案:

    def set_geo_location(self, latitude, longitude, altitude):

    logging.info("set geo location")
    try:
        # Currently Apple does not provide any API for XCTest framework to simulate GPS location
        self.driver.set_location(latitude=latitude, longitude=longitude, altitude=altitude)
    except WebDriverException:
        # this will launch Apple Script to automatically set custom GPS location on iOS simulator
        subprocess.call([os.path.join(PROJECT_ROOT, "set_geolocation_for_iOS.sh")],
                        env={"Latitude": latitude, "Longitude": longitude})  # bash cli command for iOS simulator
    sleep(2)

我正在将GPS坐标传递给该方法:

set_geo_location("-77.85", "166.66", "10")

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM