diff --git a/.github/setup/action.yml b/.github/setup/action.yml index dfcacd9594..fe37402d9e 100644 --- a/.github/setup/action.yml +++ b/.github/setup/action.yml @@ -34,9 +34,6 @@ runs: 3.1.x - if: ${{ runner.os == 'Windows' }} uses: microsoft/setup-msbuild@v1.1 - - if: ${{ runner.os == 'Windows' }} - run: choco install dotnetcore-3.1-windowshosting -y - shell: pwsh # restore packages - if: ${{ runner.os == 'Windows' }} diff --git a/.github/uitest/action.yml b/.github/uitest/action.yml index c774ce2a1d..17c43b6d09 100644 --- a/.github/uitest/action.yml +++ b/.github/uitest/action.yml @@ -31,6 +31,13 @@ runs: --config "${{ inputs.build-configuration }}" --environment "${{ inputs.runtime-environment }}" shell: bash + + - if: ${{ runner.os == 'Windows' }} + run: choco install dotnet-aspnetcoremodule-v2 -y + shell: pwsh + - if: ${{ runner.os == 'Windows' }} + run: iisreset + shell: pwsh - if: ${{ runner.os == 'Windows' }} name: uitest.ps1 run: .\.github\uitest\uitest.ps1 diff --git a/.github/uitest/uitest.ps1 b/.github/uitest/uitest.ps1 index d54c0846f7..3a1b990223 100644 --- a/.github/uitest/uitest.ps1 +++ b/.github/uitest/uitest.ps1 @@ -164,6 +164,21 @@ function Test-Sample { Sleep 5 } } + + # print out all log files + Export-IISConfiguration -PhysicalPath c:\inetpub -DontExportKeys -Force + foreach ($log in dir c:\inetpub\*.config) { + write-host $log + get-content $log | write-host + } + foreach ($log in dir c:\inetpub\logs\logfiles\*\*.log) { + write-host $log + get-content $log | write-host + } + foreach ($log in dir $root\artifacts\**\*.log) { + write-host $log + get-content $log | write-host + } throw "The sample '${sampleName}' failed to start." } } diff --git a/src/Framework/Framework/Resources/Scripts/api/api.ts b/src/Framework/Framework/Resources/Scripts/api/api.ts index 2f7fde89f2..4aee2b05a2 100644 --- a/src/Framework/Framework/Resources/Scripts/api/api.ts +++ b/src/Framework/Framework/Resources/Scripts/api/api.ts @@ -10,18 +10,39 @@ type ApiComputed = refreshValue: () => PromiseLike }; -type CachedValue = { +class CachedValue { _stateManager?: StateManager _isLoading?: boolean _promise?: PromiseLike - _referenceCount: number + _elements: HTMLElement[] = [] + + constructor(public _cache: Cache) { + } + + registerElement(element: HTMLElement, sharingKeyValue: string) { + if (!this._elements.includes(element)) { + this._elements.push(element); + + ko.utils.domNodeDisposal.addDisposeCallback(element, () => { + this.unregisterElement(element, sharingKeyValue); + }); + } + } + + unregisterElement(element: HTMLElement, sharingKeyValue: string) { + const oldElementIndex = this._elements.indexOf(element); + if (oldElementIndex >= 0) { + this._elements.splice(oldElementIndex, 1); + } + if (!this._elements.length) { + delete this._cache[sharingKeyValue]; + } + } } type Cache = { [k: string]: CachedValue } -const cachedValues: { - [key: string]: KnockoutObservable -} = {}; +const cachedValues: Cache = {}; export function invoke( target: any, @@ -33,7 +54,7 @@ export function invoke( sharingKeyProvider: (args: any[]) => string[], lifetimeElement: HTMLElement ): ApiComputed { - const cache: Cache = cacheElement ? (( cacheElement)["apiCachedValues"] ??= {}) : cachedValues; + const cache: Cache = cacheElement ? ((cacheElement)["apiCachedValues"] ??= {}) : cachedValues; const $type: TypeDefinition = { type: "dynamic" } let args: any[]; @@ -48,17 +69,14 @@ export function invoke( let oldKey = sharingKeyValue sharingKeyValue = methodName + ":" + sharingKeyProvider(args) const oldCached = cachedValue - cachedValue = cache[sharingKeyValue] ??= {_referenceCount: 0} + cachedValue = cache[sharingKeyValue] ??= new CachedValue(cache) if (cachedValue === oldCached) { return } - - cachedValue._referenceCount++ + + cachedValue.registerElement(lifetimeElement, sharingKeyValue); if (oldCached) { - oldCached._referenceCount-- - if (oldCached._referenceCount <= 0) { - delete cache[oldKey] - } + oldCached.unregisterElement(lifetimeElement, oldKey); } if (cachedValue._stateManager == null) @@ -69,6 +87,7 @@ export function invoke( } stateManager(cachedValue._stateManager) } + function reloadApi(): PromiseLike { if (!cachedValue._isLoading) { cachedValue._isLoading = true diff --git a/src/Samples/Api.AspNetCore/web.config b/src/Samples/Api.AspNetCore/web.config index 83a168d431..cfe47f27e1 100644 --- a/src/Samples/Api.AspNetCore/web.config +++ b/src/Samples/Api.AspNetCore/web.config @@ -5,7 +5,7 @@ - + diff --git a/src/Samples/Common/DotVVM.Samples.Common.csproj b/src/Samples/Common/DotVVM.Samples.Common.csproj index c91fcf80c4..bb6db75a48 100644 --- a/src/Samples/Common/DotVVM.Samples.Common.csproj +++ b/src/Samples/Common/DotVVM.Samples.Common.csproj @@ -79,6 +79,7 @@ + diff --git a/src/Samples/Common/ViewModels/FeatureSamples/Api/IncludedInPageViewModel.cs b/src/Samples/Common/ViewModels/FeatureSamples/Api/IncludedInPageViewModel.cs new file mode 100644 index 0000000000..c1769541c3 --- /dev/null +++ b/src/Samples/Common/ViewModels/FeatureSamples/Api/IncludedInPageViewModel.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using DotVVM.Framework.ViewModel; + +namespace DotVVM.Samples.Common.ViewModels.FeatureSamples.Api +{ + public class IncludedInPageViewModel : DotvvmViewModelBase + { + public bool Visible { get; set; } + + public int RefreshCounter { get; set; } + } +} + diff --git a/src/Samples/Common/Views/FeatureSamples/Api/IncludedInPage.dothtml b/src/Samples/Common/Views/FeatureSamples/Api/IncludedInPage.dothtml new file mode 100644 index 0000000000..f0059edaed --- /dev/null +++ b/src/Samples/Common/Views/FeatureSamples/Api/IncludedInPage.dothtml @@ -0,0 +1,51 @@ +@viewModel DotVVM.Samples.Common.ViewModels.FeatureSamples.Api.IncludedInPageViewModel, DotVVM.Samples.Common + + + + + + + + + + +

API and IncludeInPage

+ + + + + + + + + + {{value: Number}} + + + + + + + +

Request log

+
    +
+ + + + + + + diff --git a/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs b/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs index 9c30c1bda5..9fbc18277f 100644 --- a/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs +++ b/src/Samples/Tests/Abstractions/SamplesRouteUrls.designer.cs @@ -203,6 +203,7 @@ public partial class SamplesRouteUrls public const string FeatureSamples_Api_GetCollection = "FeatureSamples/Api/GetCollection"; public const string FeatureSamples_Api_GridViewDataSetAspNetCore = "FeatureSamples/Api/GridViewDataSetAspNetCore"; public const string FeatureSamples_Api_GridViewDataSetOwin = "FeatureSamples/Api/GridViewDataSetOwin"; + public const string FeatureSamples_Api_IncludedInPage = "FeatureSamples/Api/IncludedInPage"; public const string FeatureSamples_AttachedProperties_AttachedProperties = "FeatureSamples/AttachedProperties/AttachedProperties"; public const string FeatureSamples_Attribute_SpecialCharacters = "FeatureSamples/Attribute/SpecialCharacters"; public const string FeatureSamples_Attribute_ToStringConversion = "FeatureSamples/Attribute/ToStringConversion"; diff --git a/src/Samples/Tests/Tests/Feature/ApiTests.cs b/src/Samples/Tests/Tests/Feature/ApiTests.cs index 45dbb6cac4..433be4cc66 100644 --- a/src/Samples/Tests/Tests/Feature/ApiTests.cs +++ b/src/Samples/Tests/Tests/Feature/ApiTests.cs @@ -402,6 +402,102 @@ public void Feature_Api_CollectionOddEvenWithRestApi() }); } + [Fact] + public void Feature_Api_IncludedInPage() + { + RunInAllBrowsers(browser => { + browser.NavigateToUrl(SamplesRouteUrls.FeatureSamples_Api_IncludedInPage); + + // ensure there are no requests on page load - the dialog is hidden + browser.Wait(5000); + CheckRequests(); + + // open the dialog + browser.Single("open-static-command", SelectByDataUi).Click(); + + // check that the list of orders is loaded + CheckRequests( + "GET /api/Orders?companyId=11" + ); + var grid = browser.Single("table"); + grid.FindElements("tr").ThrowIfDifferentCountThan(11); + + // close the dialog - no additional request should appear + browser.Single("close-static-command", SelectByDataUi).Click(); + CheckRequests( + "GET /api/Orders?companyId=11" + ); + + // open the dialog again - the view should be refreshed + browser.Single("open-static-command", SelectByDataUi).Click(); + CheckRequests( + "GET /api/Orders?companyId=11", + "GET /api/Orders?companyId=11" + ); + + // close the dialog - no additional request should appear + browser.Single("close-static-command", SelectByDataUi).Click(); + CheckRequests( + "GET /api/Orders?companyId=11", + "GET /api/Orders?companyId=11" + ); + + // do the same thing with commands + browser.Single("open-command", SelectByDataUi).Click(); + CheckRequests( + "GET /api/Orders?companyId=11", + "GET /api/Orders?companyId=11", + "GET /api/Orders?companyId=11" + ); + + // close the dialog - no additional request should appear + browser.Single("close-command", SelectByDataUi).Click(); + CheckRequests( + "GET /api/Orders?companyId=11", + "GET /api/Orders?companyId=11", + "GET /api/Orders?companyId=11" + ); + + // click the refresh counter - nothing should happen since the dialog is not visible + browser.Single("refresh-counter", SelectByDataUi).Click(); + CheckRequests( + "GET /api/Orders?companyId=11", + "GET /api/Orders?companyId=11", + "GET /api/Orders?companyId=11" + ); + + // open the dialog again - the view should be refreshed + browser.Single("open-static-command", SelectByDataUi).Click(); + CheckRequests( + "GET /api/Orders?companyId=11", + "GET /api/Orders?companyId=11", + "GET /api/Orders?companyId=11", + "GET /api/Orders?companyId=11" + ); + + // click the refresh counter - now it should refresh data because the dialog is visible + browser.Single("refresh-counter", SelectByDataUi).Click(); + CheckRequests( + "GET /api/Orders?companyId=11", + "GET /api/Orders?companyId=11", + "GET /api/Orders?companyId=11", + "GET /api/Orders?companyId=11", + "GET /api/Orders?companyId=11" + ); + + void CheckRequests(params string[] expected) + { + var items = browser.FindElements("#request-log li"); + items.ThrowIfDifferentCountThan(expected.Length); + + for (var i = 0; i < expected.Length; i++) + { + AssertUI.TextEquals(items[i], expected[i]); + } + } + }); + } + public ApiTests(ITestOutputHelper output) : base(output) { }