diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c4120c..0bd921f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -65,4 +65,4 @@ jobs: dotnet test ` --configuration Release ` --no-build ` - --no-restore + --no-restore \ No newline at end of file diff --git a/.github/workflows/e2etest.yml b/.github/workflows/e2etest.yml new file mode 100644 index 0000000..cf99dd2 --- /dev/null +++ b/.github/workflows/e2etest.yml @@ -0,0 +1,179 @@ +name: "E2E: Build and Test" + +on: + schedule: + - cron: '0 10 * * 5' # Runs every Friday at 10:00 UTC + +jobs: + build_and_test: + name: Test E2E + runs-on: ubuntu-latest + defaults: + run: + shell: pwsh + + env: + ASPNETCORE_ENVIRONMENT: CI + DOTNET_CLI_TELEMETRY_OPTOUT: 1 + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 + DOTNET_NOLOGO: 1 + PROJECT_NAME: DancingGoat + DB_NAME: XByK_DancingGoat_Shopify + ASPNETCORE_URLS: https://localhost:14065 + STATUS_CHECK_URL: https://localhost:14065/status + CMSSHOPIFYCONFIG__ADMINAPIKEY: ${{ secrets.ADMIN_API_KEY }} + CMSSHOPIFYCONFIG__SHOPIFYURL: ${{ secrets.SHOPIFY_URL }} + CMSSHOPIFYCONFIG__STOREFRONTAPIKEY: ${{ secrets.STOREFRONT_API_KEY }} + CMSSHOPIFYCONFIG__STOREFRONTAPIVERSION: ${{ secrets.STOREFRONT_API_VERSION }} + CMSSHOPIFYCONFIG__STOREPASSWORD: ${{ secrets.SHOPIFY_STORE_PASSWORD }} + XPERIENCE_BY_KENTICO_LICENSE: ${{ secrets.XPERIENCE_BY_KENTICO_LICENSE }} + + steps: + - uses: actions/checkout@v4 + with: + submodules: true + + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + global-json-file: global.json + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: lts/* + + - name: Install NPM + run: | + cd test/Playwright + npm ci + npx playwright install --with-deps + cd ../.. + + - name: Install dependencies + run: | + dotnet restore ` + --locked-mode + + - name: Build Solution + run: | + dotnet build ` + --configuration Release ` + -p:XbyKVersion=* + + - name: Get Database Backup Name + run: | + $latestBackup = Get-Content -Path "./database/backups.txt" -TotalCount 1 + "DATABASE_BACKUP_FILENAME=$latestBackup" >> $env:GITHUB_ENV + + - name: Extract Database Backup + run: | + Expand-Archive ` + -Path "./database/${{ env.DATABASE_BACKUP_FILENAME }}.zip" ` + -DestinationPath "./database" + - name: Install a SQL Server suite of tools (SQLEngine, SQLPackage) + uses: potatoqualitee/mssqlsuite@9a0136e208df60b8ecb62909f076bc34854fa55a # set as a commit hash for security - v1.7 + with: + install: sqlpackage, sqlengine + sa-password: Pass@12345 + version: 2022 + + - name: Restore Database .bak + run: | + docker exec sql mkdir /var/opt/mssql/backup + docker cp "./database/${{ env.DATABASE_BACKUP_FILENAME }}" sql:/var/opt/mssql/backup + sqlcmd ` + -S localhost ` + -d master ` + -U "sa" ` + -P "Pass@12345" ` + -Q "RESTORE DATABASE [${{env.DB_NAME}}] FROM DISK='/var/opt/mssql/backup/${{ env.DATABASE_BACKUP_FILENAME }}' WITH MOVE 'XByK_DancingGoat_Shopify' TO '/var/opt/mssql/data/${{env.DB_NAME}}.mdf', MOVE 'XByK_DancingGoat_Shopify_log' TO '/var/opt/mssql/data/${{env.DB_NAME}}_log.ldf'" + if: endsWith(env.DATABASE_BACKUP_FILENAME, '.bak') + + - name: Import license key to DB + run: | + sqlcmd ` + -S localhost ` + -d ${{env.DB_NAME}} ` + -U "sa" ` + -P "Pass@12345" ` + -Q "UPDATE CMS_SettingsKey SET KeyValue='${{ env.XPERIENCE_BY_KENTICO_LICENSE }}' WHERE KeyName='CMSLicenseKey'" + + - name: Restore CI Repository + working-directory: "./scripts" + run: | + ./UpdateAndRestoreCI.ps1 + + - name: Publish Application + run: | + dotnet publish ` + ./examples/DancingGoat-Shopify ` + -c Release ` + -o ./publish ` + --no-build ` + --no-restore + + - name: Install Azurite + id: azuright + uses: potatoqualitee/azuright@e56d2754eb15218d507961493bc83ca037216887 # set as a commit hash for security - v1.1 + + - name: Test Solution + run: | + dotnet test ` + --configuration Release ` + --no-build ` + --no-restore + + - name: Run Application and E2E Tests + run: | + # Run the ASP.NET Core app as a background job + cd ./publish + Start-Job -ScriptBlock { dotnet ./${{ env.PROJECT_NAME }}.dll } -Name ${{ env.PROJECT_NAME }} + Receive-Job -Name ${{ env.PROJECT_NAME }} + cd ../ + + # The ASP.NET Core app can take a few seconds to start, so we delay running tests + # until it is ready, and fail if we go over a maximum wait time + $limit = 10 + $attempts = 0 + $success = $false + + while ($attempts -lt $limit -and -not $success) { + Start-Sleep -Seconds 1 + try { + $response = Invoke-WebRequest -Uri ${{ env.STATUS_CHECK_URL }} -Method Get -SkipCertificateCheck + if ($response.StatusCode -eq 200) { + Write-Output "Application is ready." + $success = $true + } + } + catch { + Write-Output "Attempt $attempts - Application not ready yet." + } + $attempts++ + } + + if (-not $success) { + Write-Output "Application did not respond in time." + exit 1 + } + + # Sleep for finishing initialization + Start-Sleep -Seconds 10 + + # Run the E2E tests + cd test/Playwright + npx playwright test + cd ../.. + + # Stop the background ASP.NET Core application + Receive-Job -Name ${{ env.PROJECT_NAME }} + Stop-Job -Name ${{ env.PROJECT_NAME }} + Remove-Job -Name ${{ env.PROJECT_NAME }} + + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: ./test/Playwright/playwright-report/ + retention-days: 30 diff --git a/Directory.Packages.props b/Directory.Packages.props index 7cb86ab..6a6e2b7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -1,19 +1,22 @@ true - false + true true true + true + 29.5.3 + $(LastAppliedHotfix) - - - - - + + + + + diff --git a/database/XByK_DancingGoat_Shopify.bak.zip b/database/XByK_DancingGoat_Shopify.bak.zip new file mode 100644 index 0000000..a14e8db Binary files /dev/null and b/database/XByK_DancingGoat_Shopify.bak.zip differ diff --git a/database/backups.txt b/database/backups.txt new file mode 100644 index 0000000..8068496 --- /dev/null +++ b/database/backups.txt @@ -0,0 +1 @@ +XByK_DancingGoat_Shopify.bak \ No newline at end of file diff --git a/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.alternativeform/cms.class_shopify.currencyformat/shopifycurrencyformatform.xml b/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.alternativeform/cms.class_shopify.currencyformat/shopifycurrencyformatform.xml index 35d549f..fb64547 100644 --- a/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.alternativeform/cms.class_shopify.currencyformat/shopifycurrencyformatform.xml +++ b/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.alternativeform/cms.class_shopify.currencyformat/shopifycurrencyformatform.xml @@ -1,47 +1,47 @@ - - - - Shopify.CurrencyFormat - 996f2159-f3fd-4ec1-99c6-c6d62fa12705 - cms.class - - -
- - - - Kentico.Administration.TextInput - - - - Kentico.Administration.RequiredValue - - - - - False - Currency code - False - - - - - Kentico.Administration.TextInput - $0.00 - - - False - Currency price format - - - - False - - - -
- Shopify currency format form - fc4f83bf-6a68-4c18-8e1e-77763f6ea36f - True - ShopifyCurrencyFormatForm + + + + Shopify.CurrencyFormat + 996f2159-f3fd-4ec1-99c6-c6d62fa12705 + cms.class + + +
+ + + + Kentico.Administration.TextInput + + + + Kentico.Administration.RequiredValue + + + + + False + Currency code + False + + + + + Kentico.Administration.TextInput + $0.00 + + + False + Currency price format + + + + False + + + +
+ Shopify currency format form + fc4f83bf-6a68-4c18-8e1e-77763f6ea36f + True + ShopifyCurrencyFormatForm
\ No newline at end of file diff --git a/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.class/shopify.currencyformat.xml b/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.class/shopify.currencyformat.xml index eae86eb..d66f4f3 100644 --- a/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.class/shopify.currencyformat.xml +++ b/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.class/shopify.currencyformat.xml @@ -1,31 +1,31 @@ - - - - - CurrencyCode - CurrencyCode - Shopify.CurrencyFormat - False - False - False - - - Shopify currency format - -
- - - - -
- 996f2159-f3fd-4ec1-99c6-c6d62fa12705 - False - Shopify.CurrencyFormat - - ShopifyStoreConfiguration - 3b34600d-ecb1-4048-bdf0-4ec6298ddd28 - cms.resource - - Shopify_CurrencyFormat - Other + + + + + CurrencyCode + CurrencyCode + Shopify.CurrencyFormat + False + False + False + + + Shopify currency format + +
+ + + + +
+ 996f2159-f3fd-4ec1-99c6-c6d62fa12705 + False + Shopify.CurrencyFormat + + ShopifyStoreConfiguration + 3b34600d-ecb1-4048-bdf0-4ec6298ddd28 + cms.resource + + Shopify_CurrencyFormat + Other
\ No newline at end of file diff --git a/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.class/shopify.integrationsettings.xml b/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.class/shopify.integrationsettings.xml index 4b79f26..cc786f8 100644 --- a/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.class/shopify.integrationsettings.xml +++ b/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.class/shopify.integrationsettings.xml @@ -1,23 +1,23 @@ - - - Shopify integration settings - -
- - - - - - -
- 0ae428b8-e2db-4dae-a6ce-7c317f121679 - False - Shopify.IntegrationSettings - - ShopifyStoreConfiguration - 3b34600d-ecb1-4048-bdf0-4ec6298ddd28 - cms.resource - - Shopify_IntegrationSettings - Other + + + Shopify integration settings + +
+ + + + + + +
+ 0ae428b8-e2db-4dae-a6ce-7c317f121679 + False + Shopify.IntegrationSettings + + ShopifyStoreConfiguration + 3b34600d-ecb1-4048-bdf0-4ec6298ddd28 + cms.resource + + Shopify_IntegrationSettings + Other
\ No newline at end of file diff --git a/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.contenttype/shopify.categorypage.xml b/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.contenttype/shopify.categorypage.xml index 029848b..307d63d 100644 --- a/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.contenttype/shopify.categorypage.xml +++ b/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.contenttype/shopify.categorypage.xml @@ -1,47 +1,48 @@ - - - Website - Shopify category page - -
- - - - - - False - Name - False - - - Kentico.Administration.TextInput - - - - - False - Products - False - - - Kentico.Administration.WebPageSelector - 0 - False - - - - Kentico.Administration.RequiredValue - - - - - -
- d71d0ea3-0dd7-4409-97dc-4ae738e8d77c - False - xp-box - Shopify.CategoryPage - Shopify_CategoryPage - Content - True + + + Website + Shopify category page + +
+ + + + + + False + Name + False + + + Kentico.Administration.TextInput + + + + + False + Products + False + + + Kentico.Administration.WebPageSelector + 0 + False + + + + Kentico.Administration.RequiredValue + + + + + +
+ d71d0ea3-0dd7-4409-97dc-4ae738e8d77c + False + xp-box + Shopify.CategoryPage + ShopifyCategoryPage + Shopify_CategoryPage + Content + True
\ No newline at end of file diff --git a/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.contenttype/shopify.image.xml b/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.contenttype/shopify.image.xml index 2ff1be7..40792f6 100644 --- a/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.contenttype/shopify.image.xml +++ b/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.contenttype/shopify.image.xml @@ -1,66 +1,66 @@ - - - Reusable - Shopify Image - -
- - - - - - - - - False - Image - False - - - _INHERITED_ - Kentico.Administration.ContentItemAssetUploader - - - - Kentico.Administration.RequiredValue - - - - - - - - - - False - Description - False - - - Kentico.Administration.TextInput - - - - - False - Shopify image ID - False - - - - Kentico.Administration.RequiredValue - - - - - -
- 1c9564eb-3535-4115-bee3-180063b1816b - False - xp-picture - Shopify.Image - ShopifyImage - Shopify_Image - Content - False + + + Reusable + Shopify Image + +
+ + + + + + + + + False + Image + False + + + _INHERITED_ + Kentico.Administration.ContentItemAssetUploader + + + + Kentico.Administration.RequiredValue + + + + + + + + + + False + Description + False + + + Kentico.Administration.TextInput + + + + + False + Shopify image ID + False + + + + Kentico.Administration.RequiredValue + + + + + +
+ 1c9564eb-3535-4115-bee3-180063b1816b + False + xp-picture + Shopify.Image + ShopifyImage + Shopify_Image + Content + False
\ No newline at end of file diff --git a/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.contenttype/shopify.product.xml b/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.contenttype/shopify.product.xml index 0ef8e83..08a1ab9 100644 --- a/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.contenttype/shopify.product.xml +++ b/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.contenttype/shopify.product.xml @@ -1,91 +1,91 @@ - - - Reusable - Shopify Product - -
- - - - - - - - - False - Title - False - - - Kentico.Administration.TextInput - - - - - - - - False - Description - False - - - Kentico.Administration.StructuredContent - Kentico.Administration.RichTextEditor - - - - - - - - - False - Images - False - - - - - - Kentico.Administration.ContentItemSelector - - - - - - - - False - Variants - False - - - - - - Kentico.Administration.ContentItemSelector - - - - - False - Prodcut parameters - False - - - Kentico.Administration.WebsitesStructuredContent - Kentico.Administration.RichTextEditor - - - -
- 57b30223-ca83-4d57-b271-1eb626a0fa02 - False - xp-box - Shopify.Product - ShopifyProduct - Shopify_Product - Content - False + + + Reusable + Shopify Product + +
+ + + + + + + + + False + Title + False + + + Kentico.Administration.TextInput + + + + + + + + False + Description + False + + + Kentico.Administration.StructuredContent + Kentico.Administration.RichTextEditor + + + + + + + + + False + Images + False + + + + + + Kentico.Administration.ContentItemSelector + + + + + + + + False + Variants + False + + + + + + Kentico.Administration.ContentItemSelector + + + + + False + Prodcut parameters + False + + + Kentico.Administration.WebsitesStructuredContent + Kentico.Administration.RichTextEditor + + + +
+ 57b30223-ca83-4d57-b271-1eb626a0fa02 + False + xp-box + Shopify.Product + ShopifyProduct + Shopify_Product + Content + False
\ No newline at end of file diff --git a/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.contenttype/shopify.productdetailpage.xml b/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.contenttype/shopify.productdetailpage.xml index c9e24d8..0f54c19 100644 --- a/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.contenttype/shopify.productdetailpage.xml +++ b/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.contenttype/shopify.productdetailpage.xml @@ -1,38 +1,39 @@ - - - Website - Shopify product detail page - -
- - - - - - False - Related product - False - - - - - - Kentico.Administration.ContentItemSelector - - - - Kentico.Administration.RequiredValue - - - - - -
- 55940651-33c4-488c-bbeb-6269d7dcb9e4 - False - xp-box - Shopify.ProductDetailPage - Shopify_ProductDetailPage - Content - True + + + Website + Shopify product detail page + +
+ + + + + + False + Related product + False + + + + + + Kentico.Administration.ContentItemSelector + + + + Kentico.Administration.RequiredValue + + + + + +
+ 55940651-33c4-488c-bbeb-6269d7dcb9e4 + False + xp-box + Shopify.ProductDetailPage + ShopifyProductDetailPage + Shopify_ProductDetailPage + Content + True
\ No newline at end of file diff --git a/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.contenttype/shopify.productvariant.xml b/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.contenttype/shopify.productvariant.xml index 6a0015d..436801d 100644 --- a/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.contenttype/shopify.productvariant.xml +++ b/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.contenttype/shopify.productvariant.xml @@ -1,86 +1,86 @@ - - - Reusable - Shopify Product Variant - -
- - - - - - - - - False - Title - False - - - Kentico.Administration.TextInput - - - - - - - - False - SKU number - False - - - Kentico.Administration.TextInput - - - - - - - - False - Weight - False - - - Kentico.Administration.DecimalNumberInput - - - - Kentico.Administration.MinimumDecimalValue - - 0 - - - - - - - - - - False - Image - False - - - - - - Kentico.Administration.ContentItemSelector - - - - - - -
- 8b6077af-6a9a-4cd0-ad12-a8552c54bf23 - False - xp-boxes - Shopify.ProductVariant - ShopifyProductVariant - Shopify_ProductVariant - Content - False + + + Reusable + Shopify Product Variant + +
+ + + + + + + + + False + Title + False + + + Kentico.Administration.TextInput + + + + + + + + False + SKU number + False + + + Kentico.Administration.TextInput + + + + + + + + False + Weight + False + + + Kentico.Administration.DecimalNumberInput + + + + Kentico.Administration.MinimumDecimalValue + + 0 + + + + + + + + + + False + Image + False + + + + + + Kentico.Administration.ContentItemSelector + + + + + + +
+ 8b6077af-6a9a-4cd0-ad12-a8552c54bf23 + False + xp-boxes + Shopify.ProductVariant + ShopifyProductVariant + Shopify_ProductVariant + Content + False
\ No newline at end of file diff --git a/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.contenttype/shopify.shoppingcartpage.xml b/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.contenttype/shopify.shoppingcartpage.xml index d3affad..8734071 100644 --- a/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.contenttype/shopify.shoppingcartpage.xml +++ b/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.contenttype/shopify.shoppingcartpage.xml @@ -1,29 +1,30 @@ - - - Website - Shopping cart - -
- - - - - - False - Title - False - - - Kentico.Administration.TextInput - - - -
- 8d7e5286-cb75-4d8c-b33d-b5c863497c01 - False - xp-shopping-cart - Shopify.ShoppingCartPage - Shopify_ShoppingCartPage - Content - True + + + Website + Shopping cart + +
+ + + + + + False + Title + False + + + Kentico.Administration.TextInput + + + +
+ 8d7e5286-cb75-4d8c-b33d-b5c863497c01 + False + xp-shopping-cart + Shopify.ShoppingCartPage + ShopifyShoppingCartPage + Shopify_ShoppingCartPage + Content + True
\ No newline at end of file diff --git a/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.contenttype/shopify.storepage.xml b/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.contenttype/shopify.storepage.xml index 1f936f0..1c34fc0 100644 --- a/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.contenttype/shopify.storepage.xml +++ b/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.contenttype/shopify.storepage.xml @@ -1,53 +1,54 @@ - - - Website - Store page - -
- - - - - - False - Name - False - - - Kentico.Administration.TextInput - - - - - False - Bestsellers - False - - - Kentico.Administration.WebPageSelector - 4 - False - - - - - False - Hot Tips - False - - - Kentico.Administration.WebPageSelector - 4 - False - - - -
- 09c2a667-23da-47a2-90a9-a5ab97b0fdc3 - False - xp-market - Shopify.StorePage - Shopify_StorePage - Content - True + + + Website + Store page + +
+ + + + + + False + Name + False + + + Kentico.Administration.TextInput + + + + + False + Bestsellers + False + + + Kentico.Administration.WebPageSelector + 4 + False + + + + + False + Hot Tips + False + + + Kentico.Administration.WebPageSelector + 4 + False + + + +
+ 09c2a667-23da-47a2-90a9-a5ab97b0fdc3 + False + xp-market + Shopify.StorePage + ShopifyStorePage + Shopify_StorePage + Content + True
\ No newline at end of file diff --git a/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.contenttype/shopify.thankyoupage.xml b/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.contenttype/shopify.thankyoupage.xml index 871966f..dc825c9 100644 --- a/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.contenttype/shopify.thankyoupage.xml +++ b/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.contenttype/shopify.thankyoupage.xml @@ -1,29 +1,30 @@ - - - Website - Thank you page - -
- - - - - - False - Title - False - - - Kentico.Administration.TextInput - - - -
- edc94f47-37a4-4eca-b663-acf5ed0fca28 - False - xp-check - Shopify.ThankYouPage - Shopify_ThankYouPage - Content - True + + + Website + Thank you page + +
+ + + + + + False + Title + False + + + Kentico.Administration.TextInput + + + +
+ edc94f47-37a4-4eca-b663-acf5ed0fca28 + False + xp-check + Shopify.ThankYouPage + ShopifyThankYouPage + Shopify_ThankYouPage + Content + True
\ No newline at end of file diff --git a/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.resource/shopifystoreconfiguration.xml b/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.resource/shopifystoreconfiguration.xml index d52cd1b..025453a 100644 --- a/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.resource/shopifystoreconfiguration.xml +++ b/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/cms.resource/shopifystoreconfiguration.xml @@ -1,7 +1,7 @@ - - - Shopify configuration - 3b34600d-ecb1-4048-bdf0-4ec6298ddd28 - False - ShopifyStoreConfiguration + + + Shopify configuration + 3b34600d-ecb1-4048-bdf0-4ec6298ddd28 + False + ShopifyStoreConfiguration \ No newline at end of file diff --git a/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/om.activitytype/custom_productaddedtoshoppingcart.xml b/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/om.activitytype/custom_productaddedtoshoppingcart.xml index 6c70df0..4787b65 100644 --- a/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/om.activitytype/custom_productaddedtoshoppingcart.xml +++ b/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/om.activitytype/custom_productaddedtoshoppingcart.xml @@ -1,8 +1,8 @@ - - - Product added to shopping cart - True - True - False - custom_productaddedtoshoppingcart + + + Product added to shopping cart + True + True + False + custom_productaddedtoshoppingcart \ No newline at end of file diff --git a/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/om.activitytype/custom_productremovedfromshoppingcart.xml b/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/om.activitytype/custom_productremovedfromshoppingcart.xml index 21acc3e..5516587 100644 --- a/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/om.activitytype/custom_productremovedfromshoppingcart.xml +++ b/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/om.activitytype/custom_productremovedfromshoppingcart.xml @@ -1,8 +1,8 @@ - - - Product removed from shopping cart - True - True - False - custom_productremovedfromshoppingcart + + + Product removed from shopping cart + True + True + False + custom_productremovedfromshoppingcart \ No newline at end of file diff --git a/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/om.activitytype/custom_purchase.xml b/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/om.activitytype/custom_purchase.xml index 26043f8..33a1eaa 100644 --- a/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/om.activitytype/custom_purchase.xml +++ b/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/om.activitytype/custom_purchase.xml @@ -1,8 +1,8 @@ - - - Purchase - True - True - False - custom_purchase + + + Purchase + True + True + False + custom_purchase \ No newline at end of file diff --git a/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/om.activitytype/custom_purchasedproduct.xml b/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/om.activitytype/custom_purchasedproduct.xml index 569dcbe..60a55dd 100644 --- a/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/om.activitytype/custom_purchasedproduct.xml +++ b/examples/DancingGoat-Shopify/App_Data/CIRepository/@global/om.activitytype/custom_purchasedproduct.xml @@ -1,8 +1,8 @@ - - - Purchased product - True - True - False - custom_purchasedproduct + + + Purchased product + True + True + False + custom_purchasedproduct \ No newline at end of file diff --git a/examples/DancingGoat-Shopify/DancingGoat.csproj b/examples/DancingGoat-Shopify/DancingGoat.csproj index 9f81b6f..490f3d3 100644 --- a/examples/DancingGoat-Shopify/DancingGoat.csproj +++ b/examples/DancingGoat-Shopify/DancingGoat.csproj @@ -23,11 +23,14 @@ $(DefineConstants);SEPARATED_ADMIN + + false + - - - - + + + + @@ -36,4 +39,9 @@ + + + + +
\ No newline at end of file diff --git a/examples/DancingGoat-Shopify/Program.cs b/examples/DancingGoat-Shopify/Program.cs index d3bf730..c13f35f 100644 --- a/examples/DancingGoat-Shopify/Program.cs +++ b/examples/DancingGoat-Shopify/Program.cs @@ -48,6 +48,8 @@ { options.DataAnnotationLocalizerProvider = (type, factory) => factory.Create(typeof(SharedResources)); }); + +builder.Services.AddHealthChecks(); builder.Services.AddDancingGoatServices(); @@ -55,7 +57,9 @@ builder.Services.RegisterShopifyServices(builder.Configuration); -var app = builder.Build(); +var app = builder.Build(); + +app.MapHealthChecks("/status"); app.UseSession(); @@ -67,7 +71,6 @@ app.UseAuthentication(); - app.UseKentico(); app.UseAuthorization(); diff --git a/examples/DancingGoat-Shopify/Properties/launchSettings.json b/examples/DancingGoat-Shopify/Properties/launchSettings.json index 89c676c..ccde0b1 100644 --- a/examples/DancingGoat-Shopify/Properties/launchSettings.json +++ b/examples/DancingGoat-Shopify/Properties/launchSettings.json @@ -1,28 +1,36 @@ -{ - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "XbyKUpdate": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "http://localhost:13268" - } - }, - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:14065", - "sslPort": 0 - } - } +{ + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "DancingGoat": { + "commandName": "Project", + "launchBrowser": true, + "applicationUrl": "https://localhost:14065", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "EcommerceShopify.WebCI": { + "commandName": "Project", + "launchBrowser": false, + "applicationUrl": "https://localhost:14065", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "CI" + } + } + }, + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:14065", + "sslPort": 0 + } + } } diff --git a/examples/DancingGoat-Shopify/appsettings.CI.json b/examples/DancingGoat-Shopify/appsettings.CI.json new file mode 100644 index 0000000..d71b5ac --- /dev/null +++ b/examples/DancingGoat-Shopify/appsettings.CI.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "CMSConnectionString": "Data Source=localhost;Initial Catalog=XByK_DancingGoat_Shopify;User Id=sa;Password=Pass@12345;Persist Security Info=False;Connect Timeout=10;Encrypt=False;Current Language=English;TrustServerCertificate=True;" + } +} \ No newline at end of file diff --git a/scripts/UpdateAndRestoreCI.ps1 b/scripts/UpdateAndRestoreCI.ps1 new file mode 100644 index 0000000..57699fb --- /dev/null +++ b/scripts/UpdateAndRestoreCI.ps1 @@ -0,0 +1,50 @@ +Import-Module (Resolve-Path Utilities) ` + -Function ` + Get-WebProjectPath, ` + Invoke-ExpressionWithException, ` + Write-Status ` + -Force + +function SetCIValue { + param( + [string]$ciValue + ) + + $toggleCICommand = "sqlcmd " + ` + "-S localhost " + ` + "-d $Env:DB_NAME " + ` + "-U `"sa`" " + ` + "-P `"Pass@12345`" " + ` + "-Q `"UPDATE CMS_SettingsKey SET KeyValue = N'$ciValue' WHERE KeyName = N'CMSEnableCI'`"" + Invoke-ExpressionWithException $toggleCICommand +} + +$projectPath = Get-WebProjectPath +$repositoryPath = Join-Path $projectPath "App_Data/CIRepository" +$launchProfile = $Env:ASPNETCORE_ENVIRONMENT -eq "CI" ? "EcommerceShopify.WebCI" : "DancingGoat" +$configuration = $Env:ASPNETCORE_ENVIRONMENT -eq "CI" ? "Release" : "Debug" + +$updateCommand = "dotnet run " + ` + "--launch-profile $launchProfile " + ` + "-c $configuration " + ` + "--no-build " + ` + "--project $projectPath " + ` + "--kxp-update " + ` + "--skip-confirmation" + +$restoreCommand = "dotnet run " + ` + "--launch-profile $launchProfile " + ` + "-c $configuration " + ` + "--no-build " + ` + "--no-restore " + ` + "--project $projectPath " + ` + "--kxp-ci-restore" + +SetCIValue 'False' +Invoke-ExpressionWithException $updateCommand +Invoke-ExpressionWithException $restoreCommand +SetCIValue 'True' + +Write-Host "`n" +Write-Status 'CI files processed' +Write-Host "`n" \ No newline at end of file diff --git a/scripts/Utilities/Utilities.psm1 b/scripts/Utilities/Utilities.psm1 new file mode 100644 index 0000000..2af9357 --- /dev/null +++ b/scripts/Utilities/Utilities.psm1 @@ -0,0 +1,139 @@ +# Utilities + +$scriptConfig = @{} +$scriptConfig.WorkspaceFolder = ".." +$scriptConfig.SolutionFileName = "Kentico.Xperience.Shopify.sln" +$scriptConfig.AssemblyName = "DancingGoat" + +<# + .DESCRIPTION + Returns shared configuration for PowerShell scripts +#> +function Get-ScriptConfig { + return $scriptConfig +} + +<# + .DESCRIPTION + Returns the main solution file path +#> +function Get-SolutionPath { + return Resolve-Path(Join-Path $($scriptConfig.WorkspaceFolder) $($scriptConfig.SolutionFileName)) +} + +<# + .DESCRIPTION + Returns the web application folder path from the workspace root +#> +function Get-WebProjectPath { + return Resolve-Path(Join-Path $($scriptConfig.WorkspaceFolder) "examples/DancingGoat-Shopify") +} + +<# + .DESCRIPTION + Returns the admin application folder path from the workspace root +#> +<#function Get-AdminProjectPath { + return Resolve-Path(Join-Path $($scriptConfig.WorkspaceFolder) "src/Kentico.Community.Portal.Admin") +}#> + +<# + .DESCRIPTION + Returns the admin client application folder path from the workspace root +#> +<#function Get-AdminClientProjectPath { + return Resolve-Path(Join-Path $($scriptConfig.WorkspaceFolder) "src/Kentico.Community.Portal.Admin/Client") +}#> + +<# + .DESCRIPTION + Returns the Core project folder path from the workspace root +#> +<#function Get-CoreProjectPath { + return Resolve-Path(Join-Path $($scriptConfig.WorkspaceFolder) "src/Kentico.Community.Portal.Core") +}#> + + +<# + .DESCRIPTION + Gets the database connection string from the user secrets or appsettings.json file +#> +<#function Get-ConnectionString { + $projectPath = Get-WebProjectPath + + # Try to get the connection string from user secrets first + Write-Host "Checking for a connection string user secrets for project: $projectPath" + + $connectionString = dotnet user-secrets list --project $projectPath ` + | Select-String -Pattern "ConnectionStrings:" ` + | ForEach-Object { $_.Line -replace '^ConnectionStrings:CMSConnectionString \= ', '' } + + if (-not [string]::IsNullOrEmpty($connectionString)) { + Write-Host 'Using ConnectionString from user-secrets' + + return $connectionString + } + + $appSettingFileName = $Env:ASPNETCORE_ENVIRONMENT -eq "CI" ? 'appsettings.CI.json' : 'appsettings.json' + + $jsonFilePath = Join-Path $projectPath $appSettingFileName + + Write-Host "Using settings from $jsonFilePath" + + if (!(Test-Path $jsonFilePath)) { + throw "Could not find file $jsonFilePath" + } + + $appSettingsJson = Get-Content $jsonFilePath | Out-String | ConvertFrom-Json + $connectionString = $appSettingsJson.ConnectionStrings.CMSConnectionString; + + if (!$connectionString) { + throw "Connection string not found in $jsonFilePath" + } + + return $connectionString; +}#> + +<# +.DESCRIPTION + Ensures the expression successfully exits and throws an exception + with the failed expression if it does not. +#> +function Invoke-ExpressionWithException { + param( + [string]$expression + ) + + Write-Host "$expression" + + Invoke-Expression -Command $expression + + if ($LASTEXITCODE -ne 0) { + $errorMessage = "[ $expression ] failed`n`n" + + throw $errorMessage + } +} +function Write-Status { + param( + [string]$message + ) + + Write-Host $message -ForegroundColor Blue +} + +function Write-Notification { + param( + [string]$message + ) + + Write-Host $message -ForegroundColor Magenta +} + +function Write-Error { + param( + [string]$message + ) + + Write-Host $message -ForegroundColor Red +} \ No newline at end of file diff --git a/test/Playwright/.gitignore b/test/Playwright/.gitignore new file mode 100644 index 0000000..68c5d18 --- /dev/null +++ b/test/Playwright/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/test/Playwright/README.md b/test/Playwright/README.md new file mode 100644 index 0000000..5e2dd23 --- /dev/null +++ b/test/Playwright/README.md @@ -0,0 +1,62 @@ +# Playwright E2E tests + +## Description + +E2E tests are implemented using Playwright - an open-source, NodeJS-based framework for web testing and automation. + +To prevent false-positives, tests are configured to run maximum of 2 times based on the result of the 1st test. Following combinations apply. + +- test PASSES the 1st run -> Test **PASSED** +- test FAILS the 1st run, retries and PASSES the 2nd run -> Test **PASSED** with **FLAKY** signature +- test FAILS the 1st, retries and FAILS the 2nd run -> Test **FAILED** + +## Install dependencies + +`npm i` + +`npx playwright install --with-deps` + +## Run tests + +Following env variables are expected to be set + +- ASPNETCORE_URLS = Base url of the app +- CMSSHOPIFYCONFIG\_\_ADMINAPIKEY = Shopify API access token +- CMSSHOPIFYCONFIG\_\_SHOPIFYURL = Shopify store URL +- CMSSHOPIFYCONFIG\_\_STOREPASSWORD = Shopify store password + +Run the tests with command + +`npx playwright test` + +## Test results + +Test results are generated whether the tests pass or fail. + +#### HTML + +HTML test result can be opened using + +`npx playwright show-report` + +#### JUnit + +Test results are generated also in JUnit format, for possible integration with other reporting systems. XML file can be found in `test-results/e2e-junit-results.xml` + +#### Artifacts + +In case of a test failure, following artifacts are created + +- Screenshot of the last application state (point of failure) - generates on every fail +- Video of full test run - generates on every fail +- Trace - generates on first retry + +Trace contains enhanced information to ease debugging failures. It can be opened with Trace Viewer (GUI tool) locally or using hosted variant by Playwright where it is possible to upload trace files using drag and drop. + +To open local Trace Viewer use + +`npx playwright show-trace path/to/trace.zip` + +Hosted variant by Playwright can be found at + +[trace.playwright.dev](https://trace.playwright.dev/) diff --git a/test/Playwright/package-lock.json b/test/Playwright/package-lock.json new file mode 100644 index 0000000..e24e4cf --- /dev/null +++ b/test/Playwright/package-lock.json @@ -0,0 +1,97 @@ +{ + "name": "playwright", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "playwright", + "version": "1.0.0", + "license": "ISC", + "devDependencies": { + "@playwright/test": "^1.48.2", + "@types/node": "^22.9.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.48.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.48.2.tgz", + "integrity": "sha512-54w1xCWfXuax7dz4W2M9uw0gDyh+ti/0K/MxcCUxChFh37kkdxPdfZDw5QBbuPUJHr1CiHJ1hXgSs+GgeQc5Zw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.48.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@types/node": { + "version": "22.9.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", + "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.48.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.48.2.tgz", + "integrity": "sha512-NjYvYgp4BPmiwfe31j4gHLa3J7bD2WiBz8Lk2RoSsmX38SVIARZ18VYjxLjAcDsAhA+F4iSEXTSGgjua0rrlgQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.48.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.48.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.48.2.tgz", + "integrity": "sha512-sjjw+qrLFlriJo64du+EK0kJgZzoQPsabGF4lBvsid+3CNIZIYLgnMj9V6JY5VhM2Peh20DJWIVpVljLLnlawA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/test/Playwright/package.json b/test/Playwright/package.json new file mode 100644 index 0000000..ac4d7f8 --- /dev/null +++ b/test/Playwright/package.json @@ -0,0 +1,14 @@ +{ + "name": "playwright", + "version": "1.0.0", + "main": "index.js", + "scripts": {}, + "keywords": [], + "author": "", + "license": "ISC", + "description": "", + "devDependencies": { + "@playwright/test": "^1.48.2", + "@types/node": "^22.9.0" + } +} diff --git a/test/Playwright/pageObjects/BasePage.ts b/test/Playwright/pageObjects/BasePage.ts new file mode 100644 index 0000000..5dad2da --- /dev/null +++ b/test/Playwright/pageObjects/BasePage.ts @@ -0,0 +1,36 @@ +import { type Locator, type Page } from "@playwright/test"; + +export class BasePage { + readonly page: Page; + + constructor(page: Page) { + this.page = page; + } + + async goto(url: string) { + await this.page.goto(url); + await this.fullyLoadPage(); + } + async fullyLoadPage() { + await this.page.evaluate(() => + document.querySelectorAll("img[loading=lazy]").forEach((img) => img.setAttribute("loading", "eager")) + ); + + await this.page.evaluate(async () => { + await new Promise((resolve) => { + let totalHeight = 0; + const distance = 200; + const timer = setInterval(() => { + const scrollHeight = document.body.scrollHeight; + window.scrollBy(0, distance); + totalHeight += distance; + if (totalHeight >= scrollHeight - window.innerHeight) { + clearInterval(timer); + resolve(void 0); + } + }, 70); + }); + window.scrollTo(0, 0); + }); + } +} diff --git a/test/Playwright/pageObjects/CheckoutPage.ts b/test/Playwright/pageObjects/CheckoutPage.ts new file mode 100644 index 0000000..b9958a9 --- /dev/null +++ b/test/Playwright/pageObjects/CheckoutPage.ts @@ -0,0 +1,61 @@ +import { type Locator, type Page } from "@playwright/test"; +import { BasePage } from "./BasePage"; + +export class CheckoutPage extends BasePage { + readonly $firstName: Locator; + readonly $lastName: Locator; + readonly $email: Locator; + readonly $address: Locator; + readonly $city: Locator; + readonly $psc: Locator; + readonly $country: Locator; + readonly $shippingMethods: Locator; + readonly $cardNumber: Locator; + readonly $cardExpirationDate: Locator; + readonly $cardSecurityCode: Locator; + readonly $payBtn: Locator; + + constructor(page: Page) { + super(page); + + this.$firstName = page.locator('#shippingAddressForm input[placeholder="First name (optional)"]'); + this.$lastName = page.locator('#shippingAddressForm input[placeholder="Last name"]'); + this.$email = page.locator('input[placeholder="Email or mobile phone number"]'); + this.$address = page.locator('#shippingAddressForm input[placeholder="Address"]'); + this.$city = page.locator('#shippingAddressForm input[placeholder="City"]'); + this.$psc = page.locator('#shippingAddressForm input[placeholder="Postal code"]'); + this.$country = page.locator('select[name="countryCode"]'); + this.$shippingMethods = page.locator("#shipping_methods"); + this.$cardNumber = page + .frameLocator('iframe[title="Field container for: Card number"]') + .getByPlaceholder("Card number"); + this.$cardExpirationDate = page + .frameLocator('iframe[title="Field container for: Expiration date (MM / YY)"]') + .getByPlaceholder("Expiration date (MM / YY)"); + this.$cardSecurityCode = page + .frameLocator('iframe[title="Field container for: Security code"]') + .getByPlaceholder("Security code"); + this.$payBtn = page.locator("#checkout-pay-button"); + } + + async fillCustomerDetails(details) { + await this.$firstName.fill(details.firstName); + await this.$lastName.fill(details.lastName); + await this.$email.fill(details.email); + await this.$address.fill(details.address); + await this.$psc.fill(details.psc); + await this.$city.fill(details.city); + await this.$country.selectOption({ label: details.country }); + } + async selectShipping(shipping) { + await this.$shippingMethods.locator("label", { hasText: shipping }).click(); + } + async fillPayment(payment) { + await this.$cardNumber.fill(payment.cardNumber); + await this.$cardExpirationDate.fill(payment.cardExpirationDate); + await this.$cardSecurityCode.fill(payment.cardSecurityCode); + } + async confirmOrder() { + await this.$payBtn.click(); + } +} diff --git a/test/Playwright/playwright.config.ts b/test/Playwright/playwright.config.ts new file mode 100644 index 0000000..d0b3b0b --- /dev/null +++ b/test/Playwright/playwright.config.ts @@ -0,0 +1,91 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Read environment variables from file. + * https://github.com/motdotla/dotenv + */ +// import dotenv from 'dotenv'; +// import path from 'path'; +// dotenv.config({ path: path.resolve(__dirname, '.env') }); + +/** + * See https://playwright.dev/docs/test-configuration. + */ + +const XBYK_STORE_URL = process.env.ASPNETCORE_URLS; + +export default defineConfig({ + testDir: "./tests", + snapshotPathTemplate: `{testDir}/screenshots/{projectName}/{testFilePath}/{arg}{ext}`, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + retries: 1, + /* Opt out of parallel tests on CI. */ + workers: 4, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: [["list"], ["html", { open: "never" }], ["junit", { outputFile: "test-results/e2e-junit-results.xml" }]], + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + screenshot: "only-on-failure", + video: "retain-on-failure", + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: "on-first-retry", + /* Base URL to use in actions like `await page.goto('/')`. */ + // baseURL: 'http://127.0.0.1:3000', + }, + timeout: 60 * 1000, // Maximum time a test can run + expect: { + timeout: 10000, // Maximum time expect() should wait for the condition to be met. + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: "chromium", + use: { + ...devices["Desktop Chrome"], + baseURL: XBYK_STORE_URL, // + ignoreHTTPSErrors: true, + }, + }, + + // { + // name: "firefox", + // use: { ...devices["Desktop Firefox"], baseURL: "https://localhost:14066/", ignoreHTTPSErrors: true }, + // }, + + // { + // name: "webkit", + // use: { ...devices["Desktop Safari"], baseURL: "https://localhost:14066/", ignoreHTTPSErrors: true }, + // }, + + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, + + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + // webServer: { + // command: 'npm run start', + // url: 'http://127.0.0.1:3000', + // reuseExistingServer: !process.env.CI, + // }, +}); diff --git a/test/Playwright/tests/order.spec.ts b/test/Playwright/tests/order.spec.ts new file mode 100644 index 0000000..667d052 --- /dev/null +++ b/test/Playwright/tests/order.spec.ts @@ -0,0 +1,200 @@ +import { test, expect, APIRequestContext } from "@playwright/test"; +import { CheckoutPage } from "../pageObjects/CheckoutPage"; +import { BasePage } from "../pageObjects/BasePage"; + +const SHOPIFY_ACCESS_TOKEN = process.env.CMSSHOPIFYCONFIG__ADMINAPIKEY; +const SHOPIFY_URL = process.env.CMSSHOPIFYCONFIG__SHOPIFYURL; +const SHOPIFY_STORE_PASSWORD = process.env.CMSSHOPIFYCONFIG__STOREPASSWORD; + +test.describe("Orders", () => { + let basePage: BasePage; + let apiContext: APIRequestContext; + + test.beforeAll(async ({ playwright }) => { + if (!SHOPIFY_ACCESS_TOKEN || !SHOPIFY_URL || !SHOPIFY_STORE_PASSWORD) { + throw new Error("Invalid env variable"); + } + apiContext = await playwright.request.newContext({ + baseURL: `${SHOPIFY_URL}/admin/api/2024-10/`, + extraHTTPHeaders: { + "X-Shopify-Access-Token": SHOPIFY_ACCESS_TOKEN, + }, + }); + }); + + test.afterAll(async ({}) => { + await apiContext.dispose(); + }); + + test.beforeEach(async ({ page }) => { + basePage = new BasePage(page); + await basePage.goto("/store"); + }); + + test(`Complete order with updated quantity`, async ({ page }) => { + const testData = { + customer: { + firstName: "Testfirstname", + lastName: "Testlastname", + email: "autotest@test.cz", + address: "Botanická 42/99", + city: "Brno", + psc: "602 00", + country: "Czechia", + }, + shipping: "Standard", + payment: { + cardNumber: "1", + cardExpirationDate: "12/27", + cardSecurityCode: "123", + }, + }; + test.info().annotations.push({ + type: "Info", + description: JSON.stringify(testData), + }); + + await test.step("Select category 'Brewing kits' from store page", async () => { + await page.locator(".store-menu-list li", { hasText: "Brewing kits" }).click(); + await expect(page).toHaveURL("/store/brewing-kits"); + }); + await test.step(`Select product 'Aeropress'`, async () => { + await page.locator(".product-tile", { hasText: "Aeropress" }).click(); + await expect(page.locator(".product-detail", { hasText: "Aeropress" })).toBeVisible(); // expecting to be on product page of Coffee Plunger + }); + await test.step("Add product to cart", async () => { + await page.locator("button", { hasText: "Add to cart" }).click(); + }); + await test.step("Go to shopping cart", async () => { + await page.goto("/shopping-cart"); + }); + await test.step("Update quantity", async () => { + await page.locator('input[name="Quantity"]').fill("2"); + await page.locator('input[name="cartOperation"][value="Update"]').click(); + await expect(page.locator(".cart-item-info__price")).toContainText(`1884 Kč`); + await expect(page.locator(".cart-total")).toContainText(`1884 Kč`); + }); + await test.step("Go to checkout", async () => { + await page.locator("a", { hasText: "Go to shopify checkout page" }).click(); + }); + await test.step("Login to shopify store", async () => { + if (!SHOPIFY_STORE_PASSWORD) { + throw new Error("Invalid env variable SHOPIFY_STORE_PASSWORD"); + } + await page.locator("#password").fill(SHOPIFY_STORE_PASSWORD); + await page.locator('button[type="submit"]').click(); + await page.goto("/shopping-cart"); + await page.locator("a", { hasText: "Go to shopify checkout page" }).click(); + }); + + const checkoutPage = new CheckoutPage(page); + + await test.step("Fill customer details, shipping and payment information", async () => { + await checkoutPage.fillCustomerDetails(testData.customer); + await checkoutPage.selectShipping(testData.shipping); + await checkoutPage.fillPayment(testData.payment); + }); + await test.step("Check calculated price", async () => { + await expect(page.locator('[role="row"]', { hasText: /Total/ })).toContainText("Kč 2,334.00"); // 2x Aeropress + shipping (450czk) + }); + + let orderId: string; + + await test.step("Confirm order and check thank you page", async () => { + await checkoutPage.confirmOrder(); + await expect(page.locator("#checkout-main")).toContainText("Thank you"); + await expect(page.locator('[role="row"]', { hasText: /Total/ })).toContainText("Kč 2,334.00"); + await page.waitForTimeout(3000); // explicit wait for shopify to process + await page.locator("a", { hasText: "Continue shopping" }).click(); + await expect(page).toHaveURL(/\/thank-you\?orderId=[0-9]*/); + await expect(page.locator(".thank-you-content")).toContainText("Thank You"); + orderId = await page.url().split("orderId=")[1]; + }); + + await test.step("Check order in shopify API", async () => { + const response = await apiContext.get(`./orders/${orderId}.json`, { + maxRedirects: 0, + }); + test.info().annotations.push({ + type: "Received HTTP code of shopify order", + description: response.status() + " " + response.statusText(), + }); + expect(response.ok()).toBeTruthy(); + const apiOrder = await response.json(); + expect(apiOrder).toEqual( + expect.objectContaining({ + order: expect.objectContaining({ + contact_email: testData.customer.email, + billing_address: expect.objectContaining({ + first_name: testData.customer.firstName, + address1: testData.customer.address, + phone: null, + city: testData.customer.city, + zip: testData.customer.psc, + country: "Czech Republic", + last_name: testData.customer.lastName, + country_code: "CZ", + }), + shipping_address: expect.objectContaining({ + first_name: testData.customer.firstName, + address1: testData.customer.address, + phone: null, + city: testData.customer.city, + zip: testData.customer.psc, + country: "Czech Republic", + last_name: testData.customer.lastName, + country_code: "CZ", + }), + current_total_price_set: expect.objectContaining({ + presentment_money: { + amount: "2334.00", + currency_code: "CZK", + }, + }), + total_price_set: expect.objectContaining({ + presentment_money: { + amount: "2334.00", + currency_code: "CZK", + }, + }), + line_items: [ + expect.objectContaining({ + gift_card: false, + grams: 700, + name: "Aeropress", + price_set: expect.objectContaining({ + presentment_money: { + amount: "942.00", + currency_code: "CZK", + }, + }), + quantity: 2, + sku: "SK1U", + taxable: true, + title: "Aeropress", + total_discount: "0.00", + total_discount_set: expect.objectContaining({ + presentment_money: { + amount: "0.00", + currency_code: "CZK", + }, + }), + }), + ], + shipping_lines: [ + expect.objectContaining({ + code: "Standard", + price_set: expect.objectContaining({ + presentment_money: { + amount: "450.00", + currency_code: "CZK", + }, + }), + }), + ], + }), + }) + ); + }); + }); +});